#!/usr/bin/env bun import { execSync, spawn } from 'child_process'; import * as fs from 'fs'; import { dirname, basename, join, resolve } from 'path'; import * as zlib from 'zlib'; import * as readline from 'readline'; import type { Readable } from 'stream'; type RunnerFunction = (options: RunnerOptions) => (() => void) | void; type Logger = { log: (...args: any[]) => void; trace: (...args: any[]) => void; debug: (...args: any[]) => void; info: (...args: any[]) => void; warn: (...args: any[]) => void; error: (...args: any[]) => void; }; type RunnerOptions = { logger: Logger; }; type ServiceOptions = { name: string; description: string; runner: RunnerFunction; }; export default function service(options: ServiceOptions) { const serviceName = options.name; const serviceDescription = options.description; const runner = options.runner; // Paths const lockFile = `/tmp/${serviceName}.pid`; const logDir = `/tmp/`; const logFile = join(logDir, `${serviceName}.log`); const maxLogSize = 5 * 1024 * 1024; // 5 MB per log file const maxTotalLogSize = 20 * 1024 * 1024; // 20 MB total for all logs const serviceConfig = `/etc/systemd/system/${serviceName}.service`; // Get the absolute path to the current executable const selfFile = resolve(process.argv0); const selfDir = dirname(selfFile); const selfFilename = basename(selfFile); // Function to check if systemd is available function hasSystemd() { try { execSync('which systemctl', { stdio: 'ignore' }); return fs.existsSync('/run/systemd/system'); } catch { return false; } } // Function to check if the systemd service file exists function hasSystemdService() { return fs.existsSync(serviceConfig); } // Log stream variable let logStream: fs.WriteStream | null = null; // Function to create the logger function createLogger(): Logger { // Determine if running under systemd const useSystemdLogging = hasSystemdService(); // If not using systemd, set up log file if (!useSystemdLogging) { // Open the log file for appending logStream = fs.createWriteStream(logFile, { flags: 'a' }); } // Function to write logs and handle rotation function writeLog(message: string, severity: string = 'INFO') { const formattedMessage = useSystemdLogging // Systemd includes its own timestamp ? `[${severity}] ${message}\n` // Custom timestamp : `${new Date().toISOString()} [${severity}] ${message}\n`; if (useSystemdLogging) { // Write to stdout or stderr based on severity if (severity === 'ERROR' || severity === 'WARN') { process.stderr.write(formattedMessage); } else { process.stdout.write(formattedMessage); } } else { rotateLogs(); if (logStream) { try { logStream.write(formattedMessage); } catch { // If writing fails, fallback to stdout/stderr process.stdout.write(formattedMessage); } } else { // If logStream is closed, write to stdout/stderr process.stdout.write(formattedMessage); } } } // Return the logger object const logger: Logger = { log: (...args: any[]) => { writeLog(args.join(' '), 'INFO'); }, trace: (...args: any[]) => { writeLog(args.join(' '), 'TRACE'); }, debug: (...args: any[]) => { writeLog(args.join(' '), 'DEBUG'); }, info: (...args: any[]) => { writeLog(args.join(' '), 'INFO'); }, warn: (...args: any[]) => { writeLog(args.join(' '), 'WARN'); }, error: (...args: any[]) => { writeLog(args.join(' '), 'ERROR'); }, }; return logger; } // Function to close the logger function closeLogger() { if (logStream) { logStream.end(); logStream = null; } } // Function to rotate logs function rotateLogs() { if (fs.existsSync(logFile)) { const stats = fs.statSync(logFile); if (stats.size >= maxLogSize) { // Close the existing log stream before renaming if (logStream) { logStream.end(); // Close the stream logStream = null; } // Determine the rotated log file name const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedLogFile = `${logFile}.${timestamp}`; // Rename the current log file fs.renameSync(logFile, rotatedLogFile); // Compress the rotated log file try { const gzip = zlib.createGzip(); const source = fs.createReadStream(rotatedLogFile); const destination = fs.createWriteStream(`${rotatedLogFile}.gz`); source.pipe(gzip).pipe(destination); source.on('end', () => { fs.unlinkSync(rotatedLogFile); }); } catch (error) { console.error('Error compressing log file:', error); } // Manage total log size manageTotalLogSize(); // Re-open the log stream logStream = fs.createWriteStream(logFile, { flags: 'a' }); } } } // Function to manage total log size function manageTotalLogSize() { try { const files = fs.readdirSync(logDir); const logFiles = files .filter((file) => file.startsWith(`${serviceName}.log`)) .map((file) => { const filePath = join(logDir, file); const stats = fs.lstatSync(filePath); return { name: file, path: filePath, time: stats.mtime.getTime(), size: stats.size, }; }); logFiles.sort((a, b) => b.time - a.time); // Sort by most recent // Calculate total size let totalSize = logFiles.reduce((acc, file) => acc + file.size, 0); // Delete oldest logs until total size is under maxTotalLogSize for (let i = logFiles.length - 1; i >= 0 && totalSize > maxTotalLogSize; i--) { try { fs.unlinkSync(logFiles[i].path); totalSize -= logFiles[i].size; } catch (error) { console.error('Error deleting log file:', error); } } } catch (err) { console.error('Error managing total log size:', err); } } // Daemon function function daemon() { // Create the logger const logger = createLogger(); logger.info('Daemon started...'); // Run the user-provided runner function let cleanupFunction: (() => void) | void; try { cleanupFunction = runner({ logger }); } catch (err) { logger.error('Error in runner function:', err); process.exit(1); } // Signal handling function cleanUp() { logger.info('Daemon is stopping...'); try { if (cleanupFunction) { cleanupFunction(); } } catch (err) { logger.error('Error in cleanup function:', err); } closeLogger(); try { fs.unlinkSync(lockFile); } catch { // Ignore errors } process.exit(0); } process.on('SIGTERM', cleanUp); process.on('SIGINT', cleanUp); process.on('SIGQUIT', cleanUp); process.on('uncaughtException', (err) => { logger.error('Uncaught exception:', err); cleanUp(); }); // Keep the process running process.stdin.resume(); } // Install function function install() { console.log(`Installing ${serviceName}...`); if (!hasSystemd()) { console.error('Systemd is not available on this system.'); process.exit(1); } const serviceConfigContent = ` [Unit] Description=${serviceDescription} After=network.target [Service] ExecStart=${selfFile} daemon Environment="NODE_ENV=production" "_=${selfFile}" WorkingDirectory=${selfDir} User=root Restart=always KillSignal=SIGTERM TimeoutStopSec=5 SyslogIdentifier=${serviceName} [Install] WantedBy=multi-user.target `; try { fs.writeFileSync(`/tmp/${serviceName}.service`, serviceConfigContent); execSync(`sudo mv /tmp/${serviceName}.service ${serviceConfig}`, { stdio: 'inherit', }); execSync('sudo systemctl daemon-reload', { stdio: 'inherit' }); execSync(`sudo systemctl enable ${serviceName}`, { stdio: 'inherit' }); start(); } catch (error) { console.error('Error during installation:', error); process.exit(1); } } // Uninstall function function uninstall() { console.log(`Uninstalling ${serviceName}...`); if (!hasSystemdService()) { console.error('Service is not installed via systemd.'); process.exit(1); } try { execSync(`sudo systemctl stop ${serviceName}`, { stdio: 'inherit' }); execSync(`sudo systemctl disable ${serviceName}`, { stdio: 'inherit' }); execSync(`sudo rm ${serviceConfig}`, { stdio: 'inherit' }); execSync('sudo systemctl daemon-reload', { stdio: 'inherit' }); } catch (error) { console.error('Error during uninstallation:', error); process.exit(1); } } // Start function function start() { if (hasSystemdService()) { console.log(`Starting ${serviceName} via systemd...`); try { execSync(`sudo systemctl start ${serviceName}`, { stdio: 'inherit' }); } catch (error) { console.error('Error starting service via systemd:', error); process.exit(1); } } else { console.log(`Starting ${serviceName} manually...`); if (isRunning()) { console.log(`${serviceName} is already running.`); process.exit(0); } const child = spawn(selfFile, ['daemon'], { detached: true, cwd: selfDir, stdio: ['ignore', 'ignore', 'ignore'], }); if (!child.pid) { console.error('Failed to start the service.'); process.exit(1); } fs.writeFileSync(lockFile, child.pid.toString()); console.log(`${serviceName} started with PID ${child.pid}`); child.unref(); } } // Stop function function stop() { if (hasSystemdService()) { console.log(`Stopping ${serviceName} via systemd...`); try { execSync(`sudo systemctl stop ${serviceName}`, { stdio: 'inherit' }); } catch (error) { console.error('Error stopping service via systemd:', error); process.exit(1); } } else { console.log(`Stopping ${serviceName} manually...`); if (!fs.existsSync(lockFile)) { console.log(`${serviceName} is not running.`); process.exit(0); } const pidStr = fs.readFileSync(lockFile, 'utf-8'); const pid = parseInt(pidStr, 10); try { process.kill(pid, 'SIGTERM'); console.log(`Stopped ${serviceName} with PID ${pid}.`); } catch (err) { console.error(`Failed to stop ${serviceName}:`, err); } try { fs.unlinkSync(lockFile); } catch { // Ignore errors } } } // Restart function function restart() { if (hasSystemdService()) { console.log(`Restarting ${serviceName} via systemd...`); try { execSync(`sudo systemctl restart ${serviceName}`, { stdio: 'inherit' }); } catch (error) { console.error('Error restarting service via systemd:', error); process.exit(1); } } else { console.log(`Restarting ${serviceName} manually...`); stop(); // Give some time for the process to stop setTimeout(() => { start(); }, 1000); } } // Status function function status() { if (hasSystemdService()) { try { execSync(`systemctl status ${serviceName}`, { stdio: 'inherit' }); } catch { console.log(`${serviceName} is not running via systemd.`); } } else { if (isRunning()) { const pid = fs.readFileSync(lockFile, 'utf-8'); console.log(`${serviceName} is running with PID ${pid}.`); } else { console.log(`${serviceName} is not running.`); } } } // Check if the process is running function isRunning() { try { const pidStr = fs.readFileSync(lockFile, 'utf-8'); const pid = parseInt(pidStr, 10); process.kill(pid, 0); return true; } catch { return false; } } // Function to parse command-line options function parseLogOptions(args?: string): { follow: boolean; lines: number } { const options = { follow: false, lines: 10 }; // Default values const argsArray = args ? args.split(' ') : []; argsArray.forEach((arg, index) => { if (arg === '-f' || arg === '--follow') { options.follow = true; } else if (arg === '-n' || arg === '--lines') { const value = argsArray[index + 1]; if (value && !isNaN(parseInt(value, 10))) { options.lines = parseInt(value, 10); } } }); return options; } // Custom log viewer function function logs(args?: string) { const options = parseLogOptions(args); if (hasSystemdService()) { try { execSync(`journalctl -u ${serviceName} ${args || ''}`, { stdio: 'inherit', }); } catch (error) { console.error('Error fetching logs via systemd:', error); process.exit(1); } } else { console.log(`Fetching logs manually...`); // Get all log files (including compressed ones) try { const files = fs.readdirSync(logDir); const logFiles = files .filter((file) => file.startsWith(`${serviceName}.log`)) .map((file) => { const filePath = join(logDir, file); const stats = fs.lstatSync(filePath); return { name: file, path: filePath, time: stats.mtime.getTime(), }; }); logFiles.sort((a, b) => a.time - b.time); // Sort by oldest first if (logFiles.length > 0) { if (options.follow) { // Real-time log following followLogs(); } else { // Display historical logs displayLogs(logFiles, options.lines); } } else { console.log('No logs found.'); } } catch (error) { console.error('Error fetching logs:', error); } } } // Function to display historical logs function displayLogs(logFiles: { path: string }[], lines: number) { const allLines: string[] = []; for (const file of logFiles) { let fileStream: Readable; if (file.path.endsWith('.gz')) { fileStream = fs.createReadStream(file.path).pipe(zlib.createGunzip()); } else { fileStream = fs.createReadStream(file.path); } const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity, }); rl.on('line', (line) => { allLines.push(line); }); rl.on('close', () => { // After reading all lines, display the last 'n' lines const start = Math.max(allLines.length - lines, 0); for (let i = start; i < allLines.length; i++) { console.log(allLines[i]); } }); } } // Function to follow logs in real-time function followLogs() { let currentLogFile = logFile; let position = 0; const readNewData = () => { fs.stat(currentLogFile, (err, stats) => { if (err) { // File might not exist yet return; } if (stats.size < position) { // Log file was rotated position = 0; } const stream = fs.createReadStream(currentLogFile, { encoding: 'utf-8', flags: 'r', start: position, }); stream.on('data', (chunk) => { process.stdout.write(chunk as string); position += Buffer.byteLength(chunk, 'utf-8'); }); stream.on('error', (err) => { console.error('Error reading log file:', err); }); }); }; // Initial read to get any existing data readNewData(); // Watch the current log file let watcher = fs.watch(currentLogFile, (eventType) => { if (eventType === 'change') { readNewData(); } else if (eventType === 'rename') { // Log file was rotated watcher.close(); position = 0; currentLogFile = logFile; // The new log file // Start watching the new log file watcher = fs.watch(currentLogFile, (eventType) => { if (eventType === 'change') { readNewData(); } }); } }); // Keep the process running process.stdin.resume(); } // Command-line interface const action = process.argv[2]; const args = process.argv.slice(3).join(' '); switch (action) { case 'install': install(); break; case 'uninstall': uninstall(); break; case 'start': start(); break; case 'stop': stop(); break; case 'restart': restart(); break; case 'status': status(); break; case 'logs': logs(args); break; case 'daemon': daemon(); break; default: console.log( `Usage: ${selfFilename} {install|uninstall|start|stop|restart|status|logs [args]}` ); console.log(`Description: ${serviceDescription}`); } }