/** * Daemon Manager for Onebox * * Handles background monitoring, metrics collection, and automatic tasks */ 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; private smartdaemon: plugins.smartdaemon.SmartDaemon | null = null; private running = false; private monitoringInterval: number | null = null; private metricsInterval = 60000; // 1 minute private pidFilePath: string = PID_FILE_PATH; constructor(oneboxRef: Onebox) { this.oneboxRef = oneboxRef; } /** * 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 { try { logger.info('Installing Onebox daemon service...'); // 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 { try { logger.info('Uninstalling Onebox daemon service...'); // 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 { try { if (this.running) { logger.warn('Daemon already running'); return; } logger.info('Starting Onebox daemon...'); this.running = true; // 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 { 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(); // 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 */ 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 */ 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 { 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(); // 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 { 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 { try { if (!this.oneboxRef.ssl.isConfigured()) { return; } await this.oneboxRef.ssl.renewExpiring(); } catch (error) { logger.error(`Failed to check SSL expiration: ${error.message}`); } } /** * Keep process alive */ private async keepAlive(): Promise { // 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; } /** * Write PID file */ private async writePidFile(): Promise { 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 { 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 { 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 { 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 { 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 { try { // 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'; } } }