/**
* Cross-platform service management utilities
*/
import { execSync, spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
export interface ServiceConfig {
name: string;
displayName: string;
description: string;
script: string;
user?: string;
env?: Record<string, string>;
workingDirectory?: string;
logFile?: string;
errorFile?: string;
}
export interface ServiceStatus {
installed: boolean;
running: boolean;
pid?: number;
status: string;
error?: string;
}
export class ServiceManager {
private platform: string;
private config: ServiceConfig;
constructor(config: ServiceConfig) {
this.platform = os.platform();
this.config = {
...config,
workingDirectory: config.workingDirectory || process.cwd(),
logFile: config.logFile || path.join(os.homedir(), '.mcp-fullstack', 'logs', 'service.log'),
errorFile: config.errorFile || path.join(os.homedir(), '.mcp-fullstack', 'logs', 'service.error.log')
};
// Ensure log directory exists
const logDir = path.dirname(this.config.logFile!);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
}
async install(): Promise<void> {
console.log(`📦 Installing ${this.config.displayName} service...`);
switch (this.platform) {
case 'win32':
return this.installWindows();
case 'darwin':
return this.installMacOS();
case 'linux':
return this.installLinux();
default:
throw new Error(`Platform ${this.platform} is not supported for service installation`);
}
}
async uninstall(): Promise<void> {
console.log(`🗑️ Uninstalling ${this.config.displayName} service...`);
switch (this.platform) {
case 'win32':
return this.uninstallWindows();
case 'darwin':
return this.uninstallMacOS();
case 'linux':
return this.uninstallLinux();
default:
throw new Error(`Platform ${this.platform} is not supported for service management`);
}
}
async start(): Promise<void> {
console.log(`▶️ Starting ${this.config.displayName} service...`);
switch (this.platform) {
case 'win32':
execSync(`sc start ${this.config.name}`, { stdio: 'inherit' });
break;
case 'darwin':
execSync(`launchctl load ~/Library/LaunchAgents/${this.config.name}.plist`, { stdio: 'inherit' });
break;
case 'linux':
execSync(`sudo systemctl start ${this.config.name}`, { stdio: 'inherit' });
break;
}
}
async stop(): Promise<void> {
console.log(`⏹️ Stopping ${this.config.displayName} service...`);
switch (this.platform) {
case 'win32':
execSync(`sc stop ${this.config.name}`, { stdio: 'inherit' });
break;
case 'darwin':
execSync(`launchctl unload ~/Library/LaunchAgents/${this.config.name}.plist`, { stdio: 'inherit' });
break;
case 'linux':
execSync(`sudo systemctl stop ${this.config.name}`, { stdio: 'inherit' });
break;
}
}
async restart(): Promise<void> {
console.log(`🔄 Restarting ${this.config.displayName} service...`);
try {
await this.stop();
// Wait a moment before starting
await new Promise(resolve => setTimeout(resolve, 2000));
await this.start();
} catch (error) {
console.warn('Stop failed, attempting start:', error);
await this.start();
}
}
async status(): Promise<ServiceStatus> {
switch (this.platform) {
case 'win32':
return this.getWindowsStatus();
case 'darwin':
return this.getMacOSStatus();
case 'linux':
return this.getLinuxStatus();
default:
return {
installed: false,
running: false,
status: `Platform ${this.platform} not supported`
};
}
}
async logs(lines: number = 50): Promise<string[]> {
const logFile = this.config.logFile!;
if (!fs.existsSync(logFile)) {
return ['Log file does not exist'];
}
try {
const content = fs.readFileSync(logFile, 'utf-8');
const allLines = content.split('\n').filter(line => line.trim());
return allLines.slice(-lines);
} catch (error) {
return [`Error reading log file: ${error}`];
}
}
// Windows service management
private async installWindows(): Promise<void> {
const Service = await import('node-windows');
const svc = new Service.Service({
name: this.config.name,
description: this.config.description,
script: this.config.script,
env: this.config.env,
workingDirectory: this.config.workingDirectory,
logmode: 'rotate',
logOnAs: {
domain: process.env.USERDOMAIN || '',
account: this.config.user || process.env.USERNAME || '',
password: '' // Will prompt if needed
}
});
return new Promise((resolve, reject) => {
svc.on('install', () => {
console.log('✅ Windows service installed successfully');
svc.start();
resolve();
});
svc.on('error', reject);
svc.install();
});
}
private async uninstallWindows(): Promise<void> {
const Service = await import('node-windows');
const svc = new Service.Service({
name: this.config.name,
script: this.config.script
});
return new Promise((resolve, reject) => {
svc.on('uninstall', () => {
console.log('✅ Windows service uninstalled successfully');
resolve();
});
svc.on('error', reject);
svc.uninstall();
});
}
private getWindowsStatus(): ServiceStatus {
try {
const output = execSync(`sc query ${this.config.name}`, { encoding: 'utf-8' });
const running = output.includes('STATE') && output.includes('RUNNING');
return {
installed: true,
running,
status: running ? 'running' : 'stopped'
};
} catch (error) {
return {
installed: false,
running: false,
status: 'not installed'
};
}
}
// macOS service management
private async installMacOS(): Promise<void> {
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${this.config.name}.plist`);
const plistContent = this.generateMacOSPlist();
fs.writeFileSync(plistPath, plistContent);
console.log('✅ macOS LaunchAgent installed successfully');
// Load the service
try {
execSync(`launchctl load ${plistPath}`, { stdio: 'inherit' });
} catch (error) {
console.warn('Failed to load service automatically:', error);
}
}
private async uninstallMacOS(): Promise<void> {
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${this.config.name}.plist`);
try {
execSync(`launchctl unload ${plistPath}`, { stdio: 'inherit' });
} catch (error) {
console.warn('Service was not loaded:', error);
}
if (fs.existsSync(plistPath)) {
fs.unlinkSync(plistPath);
console.log('✅ macOS LaunchAgent uninstalled successfully');
}
}
private generateMacOSPlist(): string {
const env = this.config.env || {};
const envXML = Object.entries(env)
.map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
.join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${this.config.name}</string>
<key>ProgramArguments</key>
<array>
<string>${process.execPath}</string>
<string>${this.config.script}</string>
</array>
<key>WorkingDirectory</key>
<string>${this.config.workingDirectory}</string>
<key>StandardOutPath</key>
<string>${this.config.logFile}</string>
<key>StandardErrorPath</key>
<string>${this.config.errorFile}</string>
<key>EnvironmentVariables</key>
<dict>
${envXML}
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
</dict>
</plist>`;
}
private getMacOSStatus(): ServiceStatus {
try {
const output = execSync(`launchctl list | grep ${this.config.name}`, { encoding: 'utf-8' });
const parts = output.trim().split(/\s+/);
const pid = parts[0] !== '-' ? parseInt(parts[0]) : undefined;
return {
installed: true,
running: pid !== undefined,
pid,
status: pid ? 'running' : 'stopped'
};
} catch (error) {
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${this.config.name}.plist`);
const installed = fs.existsSync(plistPath);
return {
installed,
running: false,
status: installed ? 'installed but not loaded' : 'not installed'
};
}
}
// Linux service management
private async installLinux(): Promise<void> {
const servicePath = `/etc/systemd/system/${this.config.name}.service`;
const serviceContent = this.generateLinuxService();
fs.writeFileSync(servicePath, serviceContent);
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
execSync(`sudo systemctl enable ${this.config.name}`, { stdio: 'inherit' });
console.log('✅ Linux systemd service installed successfully');
}
private async uninstallLinux(): Promise<void> {
const servicePath = `/etc/systemd/system/${this.config.name}.service`;
try {
execSync(`sudo systemctl stop ${this.config.name}`, { stdio: 'inherit' });
execSync(`sudo systemctl disable ${this.config.name}`, { stdio: 'inherit' });
} catch (error) {
console.warn('Service was not running or enabled:', error);
}
if (fs.existsSync(servicePath)) {
fs.unlinkSync(servicePath);
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
console.log('✅ Linux systemd service uninstalled successfully');
}
}
private generateLinuxService(): string {
const env = this.config.env || {};
const envLines = Object.entries(env)
.map(([key, value]) => `Environment=${key}=${value}`)
.join('\n');
return `[Unit]
Description=${this.config.description}
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=1
User=${this.config.user || 'mcp-fullstack'}
ExecStart=${process.execPath} ${this.config.script}
WorkingDirectory=${this.config.workingDirectory}
${envLines}
StandardOutput=append:${this.config.logFile}
StandardError=append:${this.config.errorFile}
[Install]
WantedBy=multi-user.target`;
}
private getLinuxStatus(): ServiceStatus {
try {
const output = execSync(`systemctl show ${this.config.name} --property=ActiveState,SubState,MainPID`, { encoding: 'utf-8' });
const lines = output.split('\n');
const activeState = lines.find(l => l.startsWith('ActiveState='))?.split('=')[1];
const subState = lines.find(l => l.startsWith('SubState='))?.split('=')[1];
const pidLine = lines.find(l => l.startsWith('MainPID='))?.split('=')[1];
const pid = pidLine && pidLine !== '0' ? parseInt(pidLine) : undefined;
return {
installed: true,
running: activeState === 'active',
pid,
status: `${activeState}/${subState}`
};
} catch (error) {
return {
installed: false,
running: false,
status: 'not installed'
};
}
}
}