- Add process.stdin.destroy() after rl.close() in all interactive commands to properly release stdin and allow process to exit cleanly - Replace raw console.log with logger methods throughout CLI handlers - Convert manual box drawing to logger.logBox() in daemon.ts - Standardize menu formatting with logger.info() and logger.dim() - Improve migration output to only show when migrations actually run Fixes issue where process would not exit after "Setup complete!" message due to stdin keeping the event loop alive.
303 lines
9.3 KiB
TypeScript
303 lines
9.3 KiB
TypeScript
import process from 'node:process';
|
|
import { execSync } from 'node:child_process';
|
|
import { Nupst } from '../nupst.ts';
|
|
import { logger } from '../logger.ts';
|
|
|
|
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void>((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<void> {
|
|
await this.nupst.getSystemd().stop();
|
|
}
|
|
|
|
/**
|
|
* Start the systemd service
|
|
*/
|
|
public async start(): Promise<void> {
|
|
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<void> {
|
|
// 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<void> {
|
|
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<void> {
|
|
try {
|
|
// Check if running as root
|
|
this.checkRootAccess(
|
|
'This command must be run as root to update NUPST.',
|
|
);
|
|
|
|
console.log('');
|
|
logger.info('Checking for updates...');
|
|
|
|
try {
|
|
// Get current version
|
|
const currentVersion = this.nupst.getVersion();
|
|
|
|
// Fetch latest version from Gitea API
|
|
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
|
|
const response = execSync(`curl -sSL ${apiUrl}`).toString();
|
|
const release = JSON.parse(response);
|
|
const latestVersion = release.tag_name; // e.g., "v4.0.7"
|
|
|
|
// Normalize versions for comparison (ensure both have "v" prefix)
|
|
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
|
|
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
|
|
|
|
logger.dim(`Current version: ${normalizedCurrent}`);
|
|
logger.dim(`Latest version: ${normalizedLatest}`);
|
|
console.log('');
|
|
|
|
// Compare normalized versions
|
|
if (normalizedCurrent === normalizedLatest) {
|
|
logger.success('Already up to date!');
|
|
console.log('');
|
|
return;
|
|
}
|
|
|
|
logger.info(`New version available: ${latestVersion}`);
|
|
logger.dim('Downloading and installing...');
|
|
console.log('');
|
|
|
|
// Download and run the install script
|
|
// This handles everything: download binary, stop service, replace, restart
|
|
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
|
|
|
|
execSync(`curl -sSL ${installUrl} | bash`, {
|
|
stdio: 'inherit', // Show install script output to user
|
|
});
|
|
|
|
console.log('');
|
|
logger.success(`Updated to ${latestVersion}`);
|
|
console.log('');
|
|
} catch (error) {
|
|
console.log('');
|
|
logger.error('Update failed');
|
|
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
|
|
console.log('');
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Update failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Completely uninstall NUPST from the system
|
|
*/
|
|
public async uninstall(): Promise<void> {
|
|
// 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<string> => {
|
|
return new Promise((resolve) => {
|
|
rl.question(question, (answer: string) => {
|
|
resolve(answer);
|
|
});
|
|
});
|
|
};
|
|
|
|
logger.log('');
|
|
logger.highlight('NUPST Uninstaller');
|
|
logger.dim('===============');
|
|
logger.log('This will completely remove NUPST from your system.');
|
|
logger.log('');
|
|
|
|
// 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) {
|
|
logger.error('Could not locate uninstall.sh script. Aborting uninstall.');
|
|
rl.close();
|
|
process.stdin.destroy();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Close readline before executing script
|
|
rl.close();
|
|
process.stdin.destroy();
|
|
|
|
// Execute uninstall.sh with the appropriate option
|
|
logger.log('');
|
|
logger.log(`Running 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) {
|
|
logger.error(`Uninstall failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
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 };
|
|
}
|
|
}
|