From a2bf4df7c2ffb635a58106e52343f490130862f4 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 16 Mar 2026 10:02:59 +0000 Subject: [PATCH] feat(systemd): replace smartdaemon-based service management with native systemd commands --- changelog.md | 7 ++ deno.json | 1 - install.sh | 12 +-- ts/00_commitinfo_data.ts | 2 +- ts/classes/daemon.ts | 96 +----------------- ts/classes/onebox.ts | 17 +--- ts/classes/systemd.ts | 185 +++++++++++++++++++++++++++++++++++ ts/cli.ts | 89 ++++++++++------- ts/index.ts | 1 + ts/plugins.ts | 4 - ts_web/00_commitinfo_data.ts | 2 +- 11 files changed, 258 insertions(+), 158 deletions(-) create mode 100644 ts/classes/systemd.ts diff --git a/changelog.md b/changelog.md index 1f9b606..b113bce 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-16 - 1.15.0 - feat(systemd) +replace smartdaemon-based service management with native systemd commands + +- adds a dedicated OneboxSystemd manager for enabling, disabling, starting, stopping, checking status, and following logs +- introduces a new `onebox systemd` CLI command set and updates install and help output to use it +- removes the smartdaemon dependency and related service management code + ## 2026-03-16 - 1.14.10 - fix(services) stop auto-update monitoring during shutdown diff --git a/deno.json b/deno.json index f44925c..c743baa 100644 --- a/deno.json +++ b/deno.json @@ -15,7 +15,6 @@ "@std/assert": "jsr:@std/assert@^1.0.15", "@std/encoding": "jsr:@std/encoding@^1.0.10", "@db/sqlite": "jsr:@db/sqlite@0.12.0", - "@push.rocks/smartdaemon": "npm:@push.rocks/smartdaemon@^2.1.0", "@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.1.1", "@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3", "@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0", diff --git a/install.sh b/install.sh index 77cddb6..03f0db1 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ SPECIFIED_VERSION="" INSTALL_DIR="/opt/onebox" GITEA_BASE_URL="https://code.foss.global" GITEA_REPO="serve.zone/onebox" -SERVICE_NAME="smartdaemon_onebox" +SERVICE_NAME="onebox" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -276,7 +276,7 @@ if [ -f "/var/lib/onebox/onebox.db" ]; then if [ $SERVICE_WAS_RUNNING -eq 1 ]; then echo "The service has been restarted with your current settings." else - echo "Start the service with: onebox daemon start" + echo "Start the service with: onebox systemd start" fi else echo "Get started:" @@ -293,11 +293,11 @@ else echo " 2. Configure ACME email:" echo " onebox config set acmeEmail " echo "" - echo " 3. Install daemon:" - echo " onebox daemon install" + echo " 3. Enable systemd service:" + echo " onebox systemd enable" echo "" - echo " 4. Start daemon:" - echo " onebox daemon start" + echo " 4. Start service:" + echo " onebox systemd start" echo "" echo " 5. Deploy your first service:" echo " onebox service add myapp --image nginx:latest --domain app.example.com" diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c35ec1b..425da3d 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/onebox', - version: '1.14.10', + version: '1.15.0', description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' } diff --git a/ts/classes/daemon.ts b/ts/classes/daemon.ts index a8d3c67..09a022d 100644 --- a/ts/classes/daemon.ts +++ b/ts/classes/daemon.ts @@ -4,9 +4,7 @@ * 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 { getErrorMessage } from '../utils/error.ts'; import type { Onebox } from './onebox.ts'; @@ -18,7 +16,6 @@ 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 statsInterval: number | null = null; @@ -46,68 +43,6 @@ export class OneboxDaemon { } } - /** - * 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: ${getErrorMessage(error)}`); - 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 services = await this.smartdaemon.systemdManager.getServices(); - const service = services.find(s => s.name === '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: ${getErrorMessage(error)}`); - throw error; - } - } - /** * Start daemon mode (background monitoring) */ @@ -482,36 +417,7 @@ export class OneboxDaemon { 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'; + throw new Error('Daemon is already running. Please stop it first with: onebox systemd stop'); } } } diff --git a/ts/classes/onebox.ts b/ts/classes/onebox.ts index 6b451c7..29694fb 100644 --- a/ts/classes/onebox.ts +++ b/ts/classes/onebox.ts @@ -14,6 +14,7 @@ import { OneboxReverseProxy } from './reverseproxy.ts'; import { OneboxDnsManager } from './dns.ts'; import { OneboxSslManager } from './ssl.ts'; import { OneboxDaemon } from './daemon.ts'; +import { OneboxSystemd } from './systemd.ts'; import { OneboxHttpServer } from './httpserver.ts'; import { CloudflareDomainSync } from './cloudflare-sync.ts'; import { CertRequirementManager } from './cert-requirement-manager.ts'; @@ -33,6 +34,7 @@ export class Onebox { public dns: OneboxDnsManager; public ssl: OneboxSslManager; public daemon: OneboxDaemon; + public systemd: OneboxSystemd; public httpServer: OneboxHttpServer; public cloudflareDomainSync: CloudflareDomainSync; public certRequirementManager: CertRequirementManager; @@ -57,6 +59,7 @@ export class Onebox { this.dns = new OneboxDnsManager(this); this.ssl = new OneboxSslManager(this); this.daemon = new OneboxDaemon(this); + this.systemd = new OneboxSystemd(); this.httpServer = new OneboxHttpServer(this); this.registry = new RegistryManager({ dataDir: './.nogit/registry-data', @@ -376,20 +379,6 @@ export class Onebox { } } - /** - * Start daemon mode - */ - async startDaemon(): Promise { - await this.daemon.start(); - } - - /** - * Stop daemon mode - */ - async stopDaemon(): Promise { - await this.daemon.stop(); - } - /** * Start OpsServer (TypedRequest-based, serves new UI) */ diff --git a/ts/classes/systemd.ts b/ts/classes/systemd.ts new file mode 100644 index 0000000..ce798a5 --- /dev/null +++ b/ts/classes/systemd.ts @@ -0,0 +1,185 @@ +/** + * 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 + +[Install] +WantedBy=multi-user.target +`; + +export class OneboxSystemd { + /** + * Install and enable the systemd service + */ + async enable(): Promise { + try { + // 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; + } + } + + /** + * 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), + }; + } +} diff --git a/ts/cli.ts b/ts/cli.ts index c89f871..9f54d51 100644 --- a/ts/cli.ts +++ b/ts/cli.ts @@ -7,6 +7,7 @@ import { projectInfo } from './info.ts'; import { getErrorMessage } from './utils/error.ts'; import { Onebox } from './classes/onebox.ts'; import { OneboxDaemon } from './classes/daemon.ts'; +import { OneboxSystemd } from './classes/systemd.ts'; export async function runCli(): Promise { const args = Deno.args; @@ -25,6 +26,19 @@ export async function runCli(): Promise { const subcommand = args[1]; try { + // === LIGHTWEIGHT COMMANDS (no init()) === + if (command === 'systemd') { + await handleSystemdCommand(subcommand, args.slice(2)); + return; + } + + if (command === 'upgrade') { + await handleUpgradeCommand(); + return; + } + + // === HEAVY COMMANDS (require full init()) === + // Server command has special handling (doesn't shut down) if (command === 'server') { const onebox = new Onebox(); @@ -60,10 +74,6 @@ export async function runCli(): Promise { await handleNginxCommand(onebox, subcommand, args.slice(2)); break; - case 'daemon': - await handleDaemonCommand(onebox, subcommand, args.slice(2)); - break; - case 'config': await handleConfigCommand(onebox, subcommand, args.slice(2)); break; @@ -72,10 +82,6 @@ export async function runCli(): Promise { await handleStatusCommand(onebox); break; - case 'upgrade': - await handleUpgradeCommand(); - break; - default: logger.error(`Unknown command: ${command}`); printHelp(); @@ -282,7 +288,7 @@ async function handleServerCommand(onebox: Onebox, args: string[]) { await OneboxDaemon.ensureNoDaemon(); } catch (error) { logger.error('Cannot start in ephemeral mode: Daemon is already running'); - logger.info('Stop the daemon first: onebox daemon stop'); + logger.info('Stop the daemon first: onebox systemd stop'); logger.info('Or run without --ephemeral to use the existing daemon'); Deno.exit(1); } @@ -326,39 +332,49 @@ async function handleServerCommand(onebox: Onebox, args: string[]) { } } -// Daemon commands -async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) { +// Systemd service commands (lightweight — no Onebox init) +async function handleSystemdCommand(subcommand: string, _args: string[]) { + const systemd = new OneboxSystemd(); + switch (subcommand) { - case 'install': - await onebox.daemon.installService(); + case 'enable': + await systemd.enable(); + break; + + case 'disable': + await systemd.disable(); break; case 'start': - await onebox.startDaemon(); + await systemd.start(); break; case 'stop': - await onebox.stopDaemon(); + await systemd.stop(); break; - case 'logs': { - const command = new Deno.Command('journalctl', { - args: ['-u', 'smartdaemon_onebox', '-f'], - stdout: 'inherit', - stderr: 'inherit', - }); - await command.output(); + case 'status': { + const status = await systemd.getStatus(); + logger.info(`Service status: ${status}`); break; } - case 'status': { - const status = await onebox.daemon.getServiceStatus(); - logger.info(`Daemon status: ${status}`); + case 'logs': + await systemd.showLogs(); + break; + + case 'start-daemon': { + // This is what systemd's ExecStart calls — full init + daemon loop + const onebox = new Onebox(); + await onebox.init(); + await onebox.daemon.start(); + // start() blocks (keepAlive loop) until SIGTERM/SIGINT break; } default: - logger.error(`Unknown daemon subcommand: ${subcommand}`); + logger.error(`Unknown systemd subcommand: ${subcommand}`); + logger.info('Available: enable, disable, start, stop, status, logs'); } } @@ -506,11 +522,12 @@ Commands: nginx test nginx status - daemon install - daemon start - daemon stop - daemon logs - daemon status + systemd enable Install and enable systemd service + systemd disable Stop, disable, and remove systemd service + systemd start Start onebox via systemctl + systemd stop Stop onebox via systemctl + systemd status Show systemd service status + systemd logs Follow service logs (journalctl) config show config set @@ -530,15 +547,15 @@ Development Workflow: onebox service add ... # In another terminal Production Workflow: - onebox daemon install # Install systemd service - onebox daemon start # Start daemon - onebox service add ... # CLI uses daemon + onebox systemd enable # Install and enable systemd service + onebox systemd start # Start via systemctl + onebox service add ... # CLI manages services Examples: onebox server --ephemeral # Start dev server onebox service add myapp --image nginx:latest --domain app.example.com --port 80 onebox registry add --url registry.example.com --username user --password pass - onebox daemon install - onebox daemon start + onebox systemd enable + onebox systemd start `); } diff --git a/ts/index.ts b/ts/index.ts index 03f69e8..350e223 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -12,6 +12,7 @@ export { OneboxReverseProxy } from './classes/reverseproxy.ts'; export { OneboxDnsManager } from './classes/dns.ts'; export { OneboxSslManager } from './classes/ssl.ts'; export { OneboxDaemon } from './classes/daemon.ts'; +export { OneboxSystemd } from './classes/systemd.ts'; export { OneboxHttpServer } from './classes/httpserver.ts'; export { OneboxApiClient } from './classes/apiclient.ts'; diff --git a/ts/plugins.ts b/ts/plugins.ts index 90a02ec..338225a 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -17,10 +17,6 @@ export { path, fs, http, encoding }; import { Database } from '@db/sqlite'; export const sqlite = { DB: Database }; -// Systemd Daemon Integration -import * as smartdaemon from '@push.rocks/smartdaemon'; -export { smartdaemon }; - // Docker API Client import { DockerHost } from '@apiclient.xyz/docker'; export const docker = { Docker: DockerHost }; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index c35ec1b..425da3d 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/onebox', - version: '1.14.10', + version: '1.15.0', description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' }