244 lines
6.7 KiB
TypeScript
244 lines
6.7 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string> {
|
|
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<void> {
|
|
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<boolean> {
|
|
try {
|
|
await Deno.stat(SERVICE_FILE_PATH);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure Docker is installed, installing it if necessary
|
|
*/
|
|
private async ensureDocker(): Promise<void> {
|
|
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),
|
|
};
|
|
}
|
|
}
|