Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user