/** * Systemd Service Manager for Onebox * * Handles systemd unit file installation, enabling, starting, stopping, * and status checking. Modeled on nupst's direct systemctl approach — * no external library dependencies. */ import { logger } from '../logging.ts'; import { getErrorMessage } from '../utils/error.ts'; const SERVICE_NAME = 'onebox'; const SERVICE_FILE_PATH = '/etc/systemd/system/onebox.service'; const SERVICE_UNIT_TEMPLATE = `[Unit] Description=Onebox - Self-hosted container platform After=network-online.target docker.service Wants=network-online.target Requires=docker.service [Service] Type=simple ExecStart=/usr/local/bin/onebox systemd start-daemon Restart=always RestartSec=10 WorkingDirectory=/var/lib/onebox Environment=PATH=/usr/bin:/usr/local/bin Environment=HOME=/root Environment=DENO_DIR=/root/.cache/deno [Install] WantedBy=multi-user.target `; export class OneboxSystemd { /** * Install and enable the systemd service */ async enable(): Promise { try { // Ensure Docker is installed before writing unit file (it requires docker.service) await this.ensureDocker(); // Write the unit file logger.info('Writing systemd unit file...'); await Deno.writeTextFile(SERVICE_FILE_PATH, SERVICE_UNIT_TEMPLATE); logger.info(`Unit file written to ${SERVICE_FILE_PATH}`); // Reload systemd daemon await this.runSystemctl(['daemon-reload']); // Enable the service const result = await this.runSystemctl(['enable', `${SERVICE_NAME}.service`]); if (!result.success) { throw new Error(`Failed to enable service: ${result.stderr}`); } logger.success('Onebox systemd service enabled'); logger.info('Start with: onebox systemd start'); } catch (error) { logger.error(`Failed to enable service: ${getErrorMessage(error)}`); throw error; } } /** * Stop, disable, and remove the systemd service */ async disable(): Promise { try { // Stop the service (ignore errors if not running) await this.runSystemctl(['stop', `${SERVICE_NAME}.service`]); // Disable the service await this.runSystemctl(['disable', `${SERVICE_NAME}.service`]); // Remove the unit file try { await Deno.remove(SERVICE_FILE_PATH); logger.info(`Removed ${SERVICE_FILE_PATH}`); } catch { // File might not exist } // Reload systemd daemon await this.runSystemctl(['daemon-reload']); logger.success('Onebox systemd service disabled and removed'); } catch (error) { logger.error(`Failed to disable service: ${getErrorMessage(error)}`); throw error; } } /** * Start the service via systemctl */ async start(): Promise { const result = await this.runSystemctl(['start', `${SERVICE_NAME}.service`]); if (!result.success) { logger.error(`Failed to start service: ${result.stderr}`); throw new Error(`Failed to start onebox service`); } logger.success('Onebox service started'); } /** * Stop the service via systemctl */ async stop(): Promise { const result = await this.runSystemctl(['stop', `${SERVICE_NAME}.service`]); if (!result.success) { logger.error(`Failed to stop service: ${result.stderr}`); throw new Error(`Failed to stop onebox service`); } logger.success('Onebox service stopped'); } /** * Get and display service status */ async getStatus(): Promise { const result = await this.runSystemctl(['status', `${SERVICE_NAME}.service`]); const output = result.stdout; let status: string; if (output.includes('active (running)')) { status = 'running'; } else if (output.includes('inactive') || output.includes('dead')) { status = 'stopped'; } else if (output.includes('failed')) { status = 'failed'; } else if (!result.success && result.stderr.includes('could not be found')) { status = 'not-installed'; } else { status = 'unknown'; } // Print the raw systemctl output for full details if (output.trim()) { console.log(output); } return status; } /** * Show service logs via journalctl */ async showLogs(): Promise { const cmd = new Deno.Command('journalctl', { args: ['-u', `${SERVICE_NAME}.service`, '-f'], stdout: 'inherit', stderr: 'inherit', }); await cmd.output(); } /** * Check if the service unit file is installed */ async isInstalled(): Promise { try { await Deno.stat(SERVICE_FILE_PATH); return true; } catch { return false; } } /** * Ensure Docker is installed, installing it if necessary */ private async ensureDocker(): Promise { try { const cmd = new Deno.Command('docker', { args: ['--version'], stdout: 'piped', stderr: 'piped', }); const result = await cmd.output(); if (result.success) { const version = new TextDecoder().decode(result.stdout).trim(); logger.info(`Docker found: ${version}`); return; } } catch { // docker command not found } logger.info('Docker not found. Installing Docker...'); const installCmd = new Deno.Command('bash', { args: ['-c', 'curl -fsSL https://get.docker.com | sh'], stdin: 'inherit', stdout: 'inherit', stderr: 'inherit', }); const installResult = await installCmd.output(); if (!installResult.success) { throw new Error('Failed to install Docker. Please install it manually: curl -fsSL https://get.docker.com | sh'); } logger.success('Docker installed successfully'); // Initialize Docker Swarm logger.info('Initializing Docker Swarm...'); const swarmCmd = new Deno.Command('docker', { args: ['swarm', 'init'], stdout: 'piped', stderr: 'piped', }); const swarmResult = await swarmCmd.output(); if (swarmResult.success) { logger.success('Docker Swarm initialized'); } else { const stderr = new TextDecoder().decode(swarmResult.stderr); if (stderr.includes('already part of a swarm')) { logger.info('Docker Swarm already initialized'); } else { logger.warn(`Docker Swarm init warning: ${stderr.trim()}`); } } } /** * Run a systemctl command and return results */ private async runSystemctl( args: string[] ): Promise<{ success: boolean; stdout: string; stderr: string }> { const cmd = new Deno.Command('systemctl', { args, stdout: 'piped', stderr: 'piped', }); const result = await cmd.output(); return { success: result.success, stdout: new TextDecoder().decode(result.stdout), stderr: new TextDecoder().decode(result.stderr), }; } }