import { execSync } from 'child_process'; import { Nupst } from '../nupst.js'; import { logger } from '../logger.js'; /** * Class for handling service-related CLI commands * Provides interface for managing systemd service */ export class ServiceHandler { private readonly nupst: Nupst; /** * Create a new Service handler * @param nupst Reference to the main Nupst instance */ constructor(nupst: Nupst) { this.nupst = nupst; } /** * Enable the service (requires root) */ public async enable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.nupst.getSystemd().install(); logger.log('NUPST service has been installed. Use "nupst start" to start the service.'); } /** * Start the daemon directly * @param debugMode Whether to enable debug mode */ public async daemonStart(debugMode: boolean = false): Promise { logger.log('Starting NUPST daemon...'); try { // Enable debug mode for SNMP if requested if (debugMode) { this.nupst.getSnmp().enableDebug(); logger.log('SNMP debug mode enabled'); } await this.nupst.getDaemon().start(); } catch (error) { // Error is already logged and process.exit is called in daemon.start() // No need to handle it here } } /** * Show logs of the systemd service */ public async logs(): Promise { try { // Use exec with spawn to properly follow logs in real-time const { spawn } = await import('child_process'); logger.log('Tailing nupst service logs (Ctrl+C to exit)...\n'); const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], { stdio: ['ignore', 'inherit', 'inherit'], }); // Forward signals to child process process.on('SIGINT', () => { journalctl.kill('SIGINT'); process.exit(0); }); // Wait for process to exit await new Promise((resolve) => { journalctl.on('exit', () => resolve()); }); } catch (error) { logger.error(`Failed to retrieve logs: ${error}`); process.exit(1); } } /** * Stop the systemd service */ public async stop(): Promise { await this.nupst.getSystemd().stop(); } /** * Start the systemd service */ public async start(): Promise { try { await this.nupst.getSystemd().start(); } catch (error) { // Error will be displayed by systemd.start() process.exit(1); } } /** * Show status of the systemd service and UPS */ public async status(): Promise { // Extract debug options from args array const debugOptions = this.extractDebugOptions(process.argv); await this.nupst.getSystemd().getStatus(debugOptions.debugMode); } /** * Disable the service (requires root) */ public async disable(): Promise { this.checkRootAccess('This command must be run as root.'); await this.nupst.getSystemd().disable(); } /** * Check if the user has root access * @param errorMessage Error message to display if not root */ private checkRootAccess(errorMessage: string): void { if (process.getuid && process.getuid() !== 0) { logger.error(errorMessage); process.exit(1); } } /** * Update NUPST from repository and refresh systemd service */ public async update(): Promise { try { // Check if running as root this.checkRootAccess( 'This command must be run as root to update NUPST and refresh the systemd service.' ); const boxWidth = 45; logger.logBoxTitle('NUPST Update Process', boxWidth); logger.logBoxLine('Updating NUPST from repository...'); // Determine the installation directory (assuming it's either /opt/nupst or the current directory) const { existsSync } = await import('fs'); let installDir = '/opt/nupst'; if (!existsSync(installDir)) { // If not installed in /opt/nupst, use the current directory const { dirname } = await import('path'); installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable logger.logBoxLine(`Using local installation directory: ${installDir}`); } try { // 1. Update the repository logger.logBoxLine('Pulling latest changes from git repository...'); execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, { stdio: 'pipe', }); // 2. Run the install.sh script logger.logBoxLine('Running install.sh to update NUPST...'); execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); // 3. Run the setup.sh script with force flag to update Node.js and dependencies logger.logBoxLine('Running setup.sh to update Node.js and dependencies...'); execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); // 4. Refresh the systemd service logger.logBoxLine('Refreshing systemd service...'); // First check if service exists let serviceExists = false; try { const output = execSync('systemctl list-unit-files | grep nupst.service').toString(); serviceExists = output.includes('nupst.service'); } catch (error) { // If grep fails (service not found), serviceExists remains false serviceExists = false; } if (serviceExists) { // Stop the service if it's running const isRunning = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; if (isRunning) { logger.logBoxLine('Stopping nupst service...'); execSync('systemctl stop nupst.service'); } // Reinstall the service logger.logBoxLine('Reinstalling systemd service...'); await this.nupst.getSystemd().install(); // Restart the service if it was running if (isRunning) { logger.logBoxLine('Restarting nupst service...'); execSync('systemctl start nupst.service'); } } else { logger.logBoxLine('Systemd service not installed, skipping service refresh.'); logger.logBoxLine('Run "nupst enable" to install the service.'); } logger.logBoxLine('Update completed successfully!'); logger.logBoxEnd(); } catch (error) { logger.logBoxLine('Error during update process:'); logger.logBoxLine(`${error.message}`); logger.logBoxEnd(); process.exit(1); } } catch (error) { logger.error(`Update failed: ${error.message}`); process.exit(1); } } /** * Completely uninstall NUPST from the system */ public async uninstall(): Promise { // Check if running as root this.checkRootAccess('This command must be run as root.'); try { // Import readline module for user input const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); // Helper function to prompt for input const prompt = (question: string): Promise => { return new Promise((resolve) => { rl.question(question, (answer: string) => { resolve(answer); }); }); }; console.log('\nNUPST Uninstaller'); console.log('==============='); console.log('This will completely remove NUPST from your system.\n'); // Ask about removing configuration const removeConfig = await prompt( 'Do you want to remove the NUPST configuration files? (y/N): ' ); // Find the uninstall.sh script location let uninstallScriptPath: string; // Try to determine script location based on executable path try { // For ESM, we can use import.meta.url, but since we might be in CJS // we'll use a more reliable approach based on process.argv[1] const binPath = process.argv[1]; const { dirname, join } = await import('path'); const modulePath = dirname(dirname(binPath)); uninstallScriptPath = join(modulePath, 'uninstall.sh'); // Check if the script exists const { access } = await import('fs/promises'); await access(uninstallScriptPath); } catch (error) { // If we can't find it in the expected location, try common installation paths const commonPaths = ['/opt/nupst/uninstall.sh', `${process.cwd()}/uninstall.sh`]; const { existsSync } = await import('fs'); uninstallScriptPath = ''; for (const path of commonPaths) { if (existsSync(path)) { uninstallScriptPath = path; break; } } if (!uninstallScriptPath) { console.error('Could not locate uninstall.sh script. Aborting uninstall.'); rl.close(); process.exit(1); } } // Close readline before executing script rl.close(); // Execute uninstall.sh with the appropriate option console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`); // Pass the configuration removal option as an environment variable const env = { ...process.env, REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no', REMOVE_REPO: 'yes', // Always remove repo as requested NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI }; // Run the uninstall script with sudo execSync(`sudo bash ${uninstallScriptPath}`, { env, stdio: 'inherit', // Show output in the terminal }); } catch (error) { console.error(`Uninstall failed: ${error.message}`); process.exit(1); } } /** * Extract and remove debug options from args array * @param args Command line arguments * @returns Object with debug flags and cleaned args */ private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } { const debugMode = args.includes('--debug') || args.includes('-d'); // Remove debug flags from args const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d'); return { debugMode, cleanedArgs }; } }