Skip to main content
Glama
service-manager.ts11.7 kB
/** * 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' }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JacobFV/mcp-fullstack'

If you have feedback or need assistance with the MCP directory API, please join our Discord server