Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
393 lines
10 KiB
JavaScript
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;
|