Files
onebox/ts/classes/daemon.ts

496 lines
13 KiB
TypeScript
Raw Normal View History

/**
* Daemon Manager for Onebox
*
* Handles background monitoring, metrics collection, and automatic tasks
*/
2025-11-18 00:03:24 +00:00
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { projectInfo } from '../info.ts';
import type { Onebox } from './onebox.ts';
// PID file constants
const PID_FILE_PATH = '/var/run/onebox/onebox.pid';
const PID_DIR = '/var/run/onebox';
const FALLBACK_PID_DIR = `${Deno.env.get('HOME')}/.onebox`;
const FALLBACK_PID_FILE = `${FALLBACK_PID_DIR}/onebox.pid`;
export class OneboxDaemon {
private oneboxRef: Onebox;
2025-11-18 00:03:24 +00:00
private smartdaemon: plugins.smartdaemon.SmartDaemon | null = null;
private running = false;
private monitoringInterval: number | null = null;
private metricsInterval = 60000; // 1 minute
2025-11-18 00:03:24 +00:00
private pidFilePath: string = PID_FILE_PATH;
private lastDomainSync = 0; // Timestamp of last Cloudflare domain sync
private domainSyncInterval = 6 * 60 * 60 * 1000; // 6 hours
constructor(oneboxRef: Onebox) {
this.oneboxRef = oneboxRef;
2025-11-18 00:03:24 +00:00
}
2025-11-18 00:03:24 +00:00
/**
* Load settings from database (call after database init)
*/
private loadSettings(): void {
try {
const customInterval = this.oneboxRef.database.getSetting('metricsInterval');
if (customInterval) {
this.metricsInterval = parseInt(customInterval, 10);
}
} catch {
// Database not initialized yet - use defaults
}
}
/**
* Install systemd service
*/
async installService(): Promise<void> {
try {
logger.info('Installing Onebox daemon service...');
2025-11-18 00:03:24 +00:00
// Initialize smartdaemon if needed
if (!this.smartdaemon) {
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
}
// Get installation directory
const execPath = Deno.execPath();
const service = await this.smartdaemon.addService({
name: 'onebox',
version: projectInfo.version,
command: `${execPath} run --allow-all ${Deno.cwd()}/mod.ts daemon start`,
description: 'Onebox - Self-hosted container platform',
workingDir: Deno.cwd(),
});
await service.save();
await service.enable();
logger.success('Onebox daemon service installed');
logger.info('Start with: sudo systemctl start smartdaemon_onebox');
} catch (error) {
logger.error(`Failed to install daemon service: ${error.message}`);
throw error;
}
}
/**
* Uninstall systemd service
*/
async uninstallService(): Promise<void> {
try {
logger.info('Uninstalling Onebox daemon service...');
2025-11-18 00:03:24 +00:00
// Initialize smartdaemon if needed
if (!this.smartdaemon) {
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
}
const service = await this.smartdaemon.getService('onebox');
if (service) {
await service.stop();
await service.disable();
await service.delete();
}
logger.success('Onebox daemon service uninstalled');
} catch (error) {
logger.error(`Failed to uninstall daemon service: ${error.message}`);
throw error;
}
}
/**
* Start daemon mode (background monitoring)
*/
async start(): Promise<void> {
try {
if (this.running) {
logger.warn('Daemon already running');
return;
}
logger.info('Starting Onebox daemon...');
this.running = true;
2025-11-18 00:03:24 +00:00
// Load settings from database
this.loadSettings();
// Write PID file
await this.writePidFile();
// Start monitoring loop
this.startMonitoring();
// Start HTTP server
const httpPort = parseInt(this.oneboxRef.database.getSetting('httpPort') || '3000', 10);
await this.oneboxRef.httpServer.start(httpPort);
logger.success('Onebox daemon started');
logger.info(`Web UI available at http://localhost:${httpPort}`);
// Keep process alive
await this.keepAlive();
} catch (error) {
logger.error(`Failed to start daemon: ${error.message}`);
this.running = false;
throw error;
}
}
/**
* Stop daemon mode
*/
async stop(): Promise<void> {
try {
if (!this.running) {
return;
}
logger.info('Stopping Onebox daemon...');
this.running = false;
// Stop monitoring
this.stopMonitoring();
// Stop HTTP server
await this.oneboxRef.httpServer.stop();
2025-11-18 00:03:24 +00:00
// Remove PID file
await this.removePidFile();
logger.success('Onebox daemon stopped');
} catch (error) {
logger.error(`Failed to stop daemon: ${error.message}`);
throw error;
}
}
/**
* Start monitoring loop
*/
2025-11-18 00:03:24 +00:00
public startMonitoring(): void {
logger.info('Starting monitoring loop...');
this.monitoringInterval = setInterval(async () => {
await this.monitoringTick();
}, this.metricsInterval);
// Run first tick immediately
this.monitoringTick();
}
/**
* Stop monitoring loop
*/
2025-11-18 00:03:24 +00:00
public stopMonitoring(): void {
if (this.monitoringInterval !== null) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
logger.debug('Monitoring loop stopped');
}
}
/**
* Single monitoring tick
*/
private async monitoringTick(): Promise<void> {
try {
logger.debug('Running monitoring tick...');
// Collect metrics for all services
await this.collectMetrics();
// Sync service statuses
await this.oneboxRef.services.syncAllServiceStatuses();
// Check SSL certificate expiration
await this.checkSSLExpiration();
// Process pending certificate requirements
await this.processCertRequirements();
// Check for certificate renewal (every tick)
await this.checkCertificateRenewal();
// Clean up old certificates (every tick, but cleanup has built-in 90-day threshold)
await this.cleanupOldCertificates();
// Sync Cloudflare domains (less frequently - every 6 hours)
await this.syncCloudflareDomainsIfNeeded();
// Check service health (TODO: implement health checks)
logger.debug('Monitoring tick complete');
} catch (error) {
logger.error(`Monitoring tick failed: ${error.message}`);
}
}
/**
* Collect metrics for all services
*/
private async collectMetrics(): Promise<void> {
try {
const services = this.oneboxRef.services.listServices();
for (const service of services) {
if (service.status === 'running' && service.containerID) {
try {
const stats = await this.oneboxRef.docker.getContainerStats(service.containerID);
if (stats) {
this.oneboxRef.database.addMetric({
serviceId: service.id!,
timestamp: Date.now(),
cpuPercent: stats.cpuPercent,
memoryUsed: stats.memoryUsed,
memoryLimit: stats.memoryLimit,
networkRxBytes: stats.networkRx,
networkTxBytes: stats.networkTx,
});
}
} catch (error) {
logger.debug(`Failed to collect metrics for ${service.name}: ${error.message}`);
}
}
}
} catch (error) {
logger.error(`Failed to collect metrics: ${error.message}`);
}
}
/**
* Check SSL certificate expiration
*/
private async checkSSLExpiration(): Promise<void> {
try {
if (!this.oneboxRef.ssl.isConfigured()) {
return;
}
await this.oneboxRef.ssl.renewExpiring();
} catch (error) {
logger.error(`Failed to check SSL expiration: ${error.message}`);
}
}
/**
* Process pending certificate requirements
*/
private async processCertRequirements(): Promise<void> {
try {
await this.oneboxRef.certRequirementManager.processPendingRequirements();
} catch (error) {
logger.error(`Failed to process cert requirements: ${error.message}`);
}
}
/**
* Check certificates for renewal (30-day threshold)
*/
private async checkCertificateRenewal(): Promise<void> {
try {
await this.oneboxRef.certRequirementManager.checkCertificateRenewal();
} catch (error) {
logger.error(`Failed to check certificate renewal: ${error.message}`);
}
}
/**
* Clean up old invalid certificates (90+ days old)
*/
private async cleanupOldCertificates(): Promise<void> {
try {
await this.oneboxRef.certRequirementManager.cleanupOldCertificates();
} catch (error) {
logger.error(`Failed to cleanup old certificates: ${error.message}`);
}
}
/**
* Sync Cloudflare domains if needed (every 6 hours)
*/
private async syncCloudflareDomainsIfNeeded(): Promise<void> {
try {
const now = Date.now();
// Check if it's time to sync (every 6 hours)
if (now - this.lastDomainSync < this.domainSyncInterval) {
return;
}
if (!this.oneboxRef.cloudflareDomainSync.isConfigured()) {
return;
}
await this.oneboxRef.cloudflareDomainSync.syncZones();
this.lastDomainSync = now;
} catch (error) {
logger.error(`Failed to sync Cloudflare domains: ${error.message}`);
}
}
/**
* Keep process alive
*/
private async keepAlive(): Promise<void> {
// Set up signal handlers
const signalHandler = () => {
logger.info('Received shutdown signal');
this.stop().then(() => {
Deno.exit(0);
});
};
Deno.addSignalListener('SIGINT', signalHandler);
Deno.addSignalListener('SIGTERM', signalHandler);
// Keep event loop alive
while (this.running) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
/**
* Get daemon status
*/
isRunning(): boolean {
return this.running;
}
2025-11-18 00:03:24 +00:00
/**
* Write PID file
*/
private async writePidFile(): Promise<void> {
try {
// Try primary location first
try {
await Deno.mkdir(PID_DIR, { recursive: true });
await Deno.writeTextFile(PID_FILE_PATH, Deno.pid.toString());
this.pidFilePath = PID_FILE_PATH;
logger.debug(`PID file written: ${PID_FILE_PATH}`);
return;
} catch (error) {
// Permission denied - try fallback location
logger.debug(`Cannot write to ${PID_DIR}, using fallback location`);
}
// Fallback to user directory
await Deno.mkdir(FALLBACK_PID_DIR, { recursive: true });
await Deno.writeTextFile(FALLBACK_PID_FILE, Deno.pid.toString());
this.pidFilePath = FALLBACK_PID_FILE;
logger.debug(`PID file written: ${FALLBACK_PID_FILE}`);
} catch (error) {
logger.warn(`Failed to write PID file: ${error.message}`);
// Non-fatal - daemon can still run
}
}
/**
* Remove PID file
*/
private async removePidFile(): Promise<void> {
try {
await Deno.remove(this.pidFilePath);
logger.debug(`PID file removed: ${this.pidFilePath}`);
} catch (error) {
// Ignore errors - file might not exist
logger.debug(`Could not remove PID file: ${error.message}`);
}
}
/**
* Check if daemon is running
*/
static async isDaemonRunning(): Promise<boolean> {
const pid = await OneboxDaemon.getDaemonPid();
if (!pid) {
return false;
}
try {
// Check if process exists
await Deno.stat(`/proc/${pid}`);
return true;
} catch {
// Process doesn't exist - clean up stale PID file
logger.debug(`Cleaning up stale PID file`);
try {
await Deno.remove(PID_FILE_PATH);
} catch {
try {
await Deno.remove(FALLBACK_PID_FILE);
} catch {
// Ignore
}
}
return false;
}
}
/**
* Get daemon PID
*/
static async getDaemonPid(): Promise<number | null> {
try {
// Try primary location
try {
const pidText = await Deno.readTextFile(PID_FILE_PATH);
return parseInt(pidText.trim(), 10);
} catch {
// Try fallback location
const pidText = await Deno.readTextFile(FALLBACK_PID_FILE);
return parseInt(pidText.trim(), 10);
}
} catch {
return null;
}
}
/**
* Ensure no daemon is running
*/
static async ensureNoDaemon(): Promise<void> {
const running = await OneboxDaemon.isDaemonRunning();
if (running) {
throw new Error('Daemon is already running. Please stop it first with: onebox daemon stop');
}
}
/**
* Get service status from systemd
*/
async getServiceStatus(): Promise<string> {
try {
2025-11-18 00:03:24 +00:00
// Don't need smartdaemon to check status, just use systemctl directly
const command = new Deno.Command('systemctl', {
args: ['status', 'smartdaemon_onebox'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
return 'not-installed';
}
}
}