Files
dashcaddy/dashcaddy-installer/src/main/service-manager.js
T
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

393 lines
10 KiB
JavaScript

/**
* Service Manager
* Handles starting, stopping, and managing Caddy and Docker services
*/
const { exec, spawn } = require('child_process');
const { promisify } = require('util');
const path = require('path');
const fs = require('fs').promises;
const platformUtils = require('../shared/platform-utils');
const execAsync = promisify(exec);
class ServiceManager {
constructor() {
this.platform = platformUtils.getPlatformInfo().os;
}
/**
* Start Caddy with the specified Caddyfile
* @param {string} caddyfilePath - Path to Caddyfile
* @param {string} caddyBinaryPath - Optional path to Caddy binary
* @returns {Promise<Object>} { success: boolean, message: string }
*/
async startCaddy(caddyfilePath, caddyBinaryPath) {
try {
const caddyCmd = caddyBinaryPath || 'caddy';
// Check if Caddyfile exists
try {
await fs.access(caddyfilePath);
} catch {
return {
success: false,
error: 'Caddyfile not found'
};
}
if (this.platform === 'windows') {
// On Windows, start Caddy in background
const caddy = spawn(caddyCmd, ['run', '--config', caddyfilePath], {
detached: true,
stdio: 'ignore',
windowsHide: true
});
caddy.unref();
// Wait a moment for Caddy to start
await new Promise(resolve => setTimeout(resolve, 2000));
// Check if Caddy is running
const status = await this.checkCaddyStatus();
return {
success: status.running,
message: status.running ? 'Caddy started successfully' : 'Caddy may have failed to start',
pid: caddy.pid
};
} else {
// On Unix, use systemd if available, otherwise start directly
const systemdAvailable = await this.isSystemdAvailable();
if (systemdAvailable) {
// Create systemd service file
await this.createCaddySystemdService(caddyfilePath, caddyBinaryPath);
// Start the service
await execAsync('sudo systemctl daemon-reload');
await execAsync('sudo systemctl enable caddy');
await execAsync('sudo systemctl start caddy');
return {
success: true,
message: 'Caddy started via systemd'
};
} else {
// Start directly
const caddy = spawn(caddyCmd, ['run', '--config', caddyfilePath], {
detached: true,
stdio: 'ignore'
});
caddy.unref();
await new Promise(resolve => setTimeout(resolve, 2000));
return {
success: true,
message: 'Caddy started in background',
pid: caddy.pid
};
}
}
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Stop Caddy
* @returns {Promise<Object>} { success: boolean, message: string }
*/
async stopCaddy() {
try {
if (this.platform === 'windows') {
await execAsync('taskkill /IM caddy.exe /F');
} else {
const systemdAvailable = await this.isSystemdAvailable();
if (systemdAvailable) {
await execAsync('sudo systemctl stop caddy');
} else {
await execAsync('pkill caddy');
}
}
return {
success: true,
message: 'Caddy stopped'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Check if Caddy is running
* @returns {Promise<Object>} { running: boolean }
*/
async checkCaddyStatus() {
try {
if (this.platform === 'windows') {
const result = await execAsync('tasklist /FI "IMAGENAME eq caddy.exe"');
const running = result.stdout.includes('caddy.exe');
return { running };
} else {
const result = await execAsync('pgrep caddy');
return { running: result.stdout.trim().length > 0 };
}
} catch {
return { running: false };
}
}
/**
* Start Docker Compose services
* @param {string} installPath - Installation directory containing docker-compose.yml
* @returns {Promise<Object>} { success: boolean, message: string }
*/
async startDockerCompose(installPath) {
try {
const composePath = path.join(installPath, 'sites', 'dashcaddy-api', 'docker-compose.yml');
// Check if docker-compose.yml exists
try {
await fs.access(composePath);
} catch {
return {
success: false,
error: 'docker-compose.yml not found'
};
}
// Start containers (cwd must be the compose directory for build context)
const composeDir = path.dirname(composePath);
await execAsync(`docker compose -f "${composePath}" up -d --build`, {
cwd: composeDir,
timeout: 300000 // 5 minute timeout for initial build
});
return {
success: true,
message: 'Docker containers started'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Stop Docker Compose services
* @param {string} installPath - Installation directory containing docker-compose.yml
* @returns {Promise<Object>} { success: boolean, message: string }
*/
async stopDockerCompose(installPath) {
try {
const composePath = path.join(installPath, 'sites', 'dashcaddy-api', 'docker-compose.yml');
await execAsync(`docker compose -f "${composePath}" down`, {
cwd: path.dirname(composePath),
timeout: 60000
});
return {
success: true,
message: 'Docker containers stopped'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Check Docker Compose status
* @param {string} installPath - Installation directory
* @returns {Promise<Object>} { running: boolean, containers: array }
*/
async checkDockerComposeStatus(installPath) {
try {
const composePath = path.join(installPath, 'sites', 'dashcaddy-api', 'docker-compose.yml');
const result = await execAsync(`docker compose -f "${composePath}" ps --format json`, {
cwd: path.dirname(composePath)
});
const containers = result.stdout.trim()
.split('\n')
.filter(line => line.trim())
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(Boolean);
return {
running: containers.some(c => c.State === 'running'),
containers
};
} catch {
return {
running: false,
containers: []
};
}
}
/**
* Start all services
* @param {string} installPath - Installation directory
* @param {Object} options - Options including caddyfilePath
* @returns {Promise<Object>} { success: boolean, services: object }
*/
async startAll(installPath, options = {}) {
const results = {
caddy: null,
docker: null
};
// Start Caddy
const caddyfilePath = options.caddyfilePath || path.join(installPath, 'Caddyfile');
results.caddy = await this.startCaddy(caddyfilePath, options.caddyBinaryPath);
// Start Docker Compose
results.docker = await this.startDockerCompose(installPath);
return {
success: results.caddy.success && results.docker.success,
services: results
};
}
/**
* Stop all services
* @param {string} installPath - Installation directory
* @returns {Promise<Object>} { success: boolean, services: object }
*/
async stopAll(installPath) {
const results = {
caddy: null,
docker: null
};
results.caddy = await this.stopCaddy();
results.docker = await this.stopDockerCompose(installPath);
return {
success: results.caddy.success && results.docker.success,
services: results
};
}
/**
* Check if systemd is available
* @returns {Promise<boolean>}
*/
async isSystemdAvailable() {
try {
await execAsync('systemctl --version');
return true;
} catch {
return false;
}
}
/**
* Create systemd service file for Caddy
* @param {string} caddyfilePath - Path to Caddyfile
* @param {string} caddyBinaryPath - Path to Caddy binary
*/
async createCaddySystemdService(caddyfilePath, caddyBinaryPath) {
const caddyCmd = caddyBinaryPath || '/usr/bin/caddy';
const serviceContent = `[Unit]
Description=Caddy Web Server (DashCaddy)
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=${caddyCmd} run --config ${caddyfilePath}
ExecReload=${caddyCmd} reload --config ${caddyfilePath}
Restart=on-failure
RestartSec=5
LimitNOFILE=1048576
LimitNPROC=512
[Install]
WantedBy=multi-user.target
`;
const servicePath = '/etc/systemd/system/caddy.service';
// Write service file (requires sudo)
await execAsync(`echo '${serviceContent}' | sudo tee ${servicePath}`);
}
/**
* Check if Docker daemon is running
* @returns {Promise<boolean>}
*/
async isDockerRunning() {
try {
await execAsync('docker info');
return true;
} catch {
return false;
}
}
/**
* Wait for a service to become available
* @param {string} url - URL to check
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<boolean>}
*/
async waitForService(url, timeout = 30000) {
const startTime = Date.now();
const http = require('http');
const https = require('https');
while (Date.now() - startTime < timeout) {
try {
await new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
const req = protocol.get(url, (res) => {
resolve(res.statusCode);
});
req.on('error', reject);
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
return true;
} catch {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
return false;
}
}
module.exports = ServiceManager;