#! /usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { maybeRunCleanup } from './database/cleanup.ts'; import { createProcessEntry, deleteProcessEntry } from './database/processTable.ts'; import type { LogCollectorLaunchInfo } from './log-collector/LogCollectorLaunchInfo.ts'; import { readStdinAsJson } from './log-collector/readStdinJson.ts'; import { startMonitoredService } from './log-collector/startMonitoredService.ts'; import { saveProcessLog } from './logs/processLogs.ts'; import { ProcessLogType } from './logs/ProcessLogType.ts'; import { debugLog } from './debug.ts'; import { getStateDirectory } from './dirs.ts'; import * as Path from 'node:path'; const DEFAULT_GRACE_PERIOD_WAIT_MS = 500; async function getLaunchInfo(): Promise { const args = hideBin(process.argv); if (args.length === 0) { return readStdinAsJson(); } const parsed = await yargs(args) .option('commandName', { type: 'string', demandOption: true, description: 'Name of the command/service', }) .option('projectDir', { type: 'string', demandOption: true, description: 'Project directory', }) .option('shell', { type: 'string', demandOption: true, description: 'Shell command to run', }) .option('root', { type: 'string', description: 'Root directory relative to projectDir', }) .option('enableStdin', { type: 'boolean', default: false, description: 'Enable stdin message polling from database', }) .option('databasePath', { type: 'string', description: 'Path to the SQLite database file', }) .parse(); return { commandName: parsed.commandName, projectDir: Path.resolve(parsed.projectDir), shell: parsed.shell, root: parsed.root, enableStdin: parsed.enableStdin, databasePath: parsed.databasePath ?? Path.join(getStateDirectory(), 'candle.db'), }; } async function main() { const launchInfo = await getLaunchInfo(); // Check for cleanup on an interval. setInterval(maybeRunCleanup, 60 * 1000); debugLog('[main-log-collector] Got launchInfo: ' + JSON.stringify(launchInfo)); const subprocess = startMonitoredService(launchInfo); const pid = subprocess.proc.pid!; debugLog('[main-log-collector] Launched subprocess, pid=' + pid); createProcessEntry({ commandName: launchInfo.commandName, projectDir: launchInfo.projectDir, pid, logCollectorPid: process.pid, shell: launchInfo.shell, root: launchInfo.root, }); try { await subprocess.waitForStart(); } catch (error) { debugLog('[main-log-collector] Process failed to start, pid=' + pid + ', error=' + error.message); saveProcessLog({ command_name: launchInfo.commandName, project_dir: launchInfo.projectDir, log_type: ProcessLogType.process_start_failed, content: 'Process failed to start: ' + error.message, }); process.exit(1); } // Grace period: Wait for a short period to ensure the process does not fail quickly. await new Promise(resolve => setTimeout(resolve, DEFAULT_GRACE_PERIOD_WAIT_MS)); if (subprocess.proc.exitCode != null && subprocess.proc.exitCode !== 0) { debugLog('[main-log-collector] Process failed during grace period, pid=' + pid + ', code=' + subprocess.proc.exitCode); saveProcessLog({ command_name: launchInfo.commandName, project_dir: launchInfo.projectDir, log_type: ProcessLogType.process_start_failed, content: 'Process failed to start: ' + subprocess.proc.exitCode, }); deleteProcessEntry({ commandName: launchInfo.commandName, projectDir: launchInfo.projectDir, pid, }); return; } debugLog('[main-log-collector] Process started, pid=' + pid); saveProcessLog({ command_name: launchInfo.commandName, project_dir: launchInfo.projectDir, log_type: ProcessLogType.process_started, }); await subprocess.waitForExit(); debugLog('[main-log-collector] Process exited, pid=' + pid + ', code=' + subprocess.proc.exitCode); saveProcessLog({ command_name: launchInfo.commandName, project_dir: launchInfo.projectDir, log_type: ProcessLogType.process_exited, content: 'Process exited with code ' + subprocess.proc.exitCode, }); deleteProcessEntry({ commandName: launchInfo.commandName, projectDir: launchInfo.projectDir, pid, }); } main().catch(error => { console.error(error); process.exit(1); });