feat: add daemon skeleton with lifecycle management
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
import { Lifecycle } from './lifecycle.js';
|
||||||
|
import type { Config } from '../config/index.js';
|
||||||
|
|
||||||
|
export interface DaemonContext {
|
||||||
|
config: Config;
|
||||||
|
lifecycle: Lifecycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startDaemon(config: Config): Promise<DaemonContext> {
|
||||||
|
const lifecycle = new Lifecycle();
|
||||||
|
|
||||||
|
// Register signal handlers
|
||||||
|
const signalHandler = () => {
|
||||||
|
lifecycle.shutdown().then(() => process.exit(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', signalHandler);
|
||||||
|
process.on('SIGTERM', signalHandler);
|
||||||
|
|
||||||
|
lifecycle.onShutdown(async () => {
|
||||||
|
process.off('SIGINT', signalHandler);
|
||||||
|
process.off('SIGTERM', signalHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Flynn daemon started');
|
||||||
|
|
||||||
|
return { config, lifecycle };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Lifecycle } from './lifecycle.js';
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Lifecycle } from './lifecycle.js';
|
||||||
|
|
||||||
|
describe('Lifecycle', () => {
|
||||||
|
it('registers and calls shutdown handlers in reverse order', async () => {
|
||||||
|
const lifecycle = new Lifecycle();
|
||||||
|
const calls: number[] = [];
|
||||||
|
|
||||||
|
lifecycle.onShutdown(async () => { calls.push(1); });
|
||||||
|
lifecycle.onShutdown(async () => { calls.push(2); });
|
||||||
|
lifecycle.onShutdown(async () => { calls.push(3); });
|
||||||
|
|
||||||
|
await lifecycle.shutdown();
|
||||||
|
|
||||||
|
expect(calls).toEqual([3, 2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only shuts down once', async () => {
|
||||||
|
const lifecycle = new Lifecycle();
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
lifecycle.onShutdown(async () => { count++; });
|
||||||
|
|
||||||
|
await lifecycle.shutdown();
|
||||||
|
await lifecycle.shutdown();
|
||||||
|
|
||||||
|
expect(count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports running state', async () => {
|
||||||
|
const lifecycle = new Lifecycle();
|
||||||
|
|
||||||
|
expect(lifecycle.isRunning).toBe(true);
|
||||||
|
|
||||||
|
await lifecycle.shutdown();
|
||||||
|
|
||||||
|
expect(lifecycle.isRunning).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
type ShutdownHandler = () => Promise<void>;
|
||||||
|
|
||||||
|
export class Lifecycle {
|
||||||
|
private shutdownHandlers: ShutdownHandler[] = [];
|
||||||
|
private shuttingDown = false;
|
||||||
|
private _isRunning = true;
|
||||||
|
|
||||||
|
get isRunning(): boolean {
|
||||||
|
return this._isRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
onShutdown(handler: ShutdownHandler): void {
|
||||||
|
this.shutdownHandlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown(): Promise<void> {
|
||||||
|
if (this.shuttingDown) return;
|
||||||
|
this.shuttingDown = true;
|
||||||
|
this._isRunning = false;
|
||||||
|
|
||||||
|
console.log('Shutting down...');
|
||||||
|
|
||||||
|
// Execute handlers in reverse order (LIFO)
|
||||||
|
for (const handler of [...this.shutdownHandlers].reverse()) {
|
||||||
|
try {
|
||||||
|
await handler();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Shutdown handler error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Shutdown complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,30 @@
|
|||||||
|
import { loadConfig } from './config/index.js';
|
||||||
|
import { startDaemon } from './daemon/index.js';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
|
||||||
|
const CONFIG_PATH = process.env.FLYNN_CONFIG
|
||||||
|
?? resolve(homedir(), '.config/flynn/config.yaml');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
console.log('Flynn starting...');
|
console.log('Flynn starting...');
|
||||||
|
console.log(`Loading config from: ${CONFIG_PATH}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = loadConfig(CONFIG_PATH);
|
||||||
|
const daemon = await startDaemon(config);
|
||||||
|
|
||||||
|
console.log(`Telegram bot configured for chat IDs: ${config.telegram.allowed_chat_ids.join(', ')}`);
|
||||||
|
console.log(`Server port: ${config.server.port}`);
|
||||||
|
|
||||||
|
// Keep process alive
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
daemon.lifecycle.onShutdown(async () => resolve());
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start Flynn:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|||||||
Reference in New Issue
Block a user