/** * 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} { 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} { 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} { 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} { 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} { 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} { 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} { 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} { 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} */ 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} */ 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} */ 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;