| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642 |
- #!/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}`);
- }
- }
|