From 513da621c67ef921c1e1b016bf1c7db040b30d83 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 12 Aug 2026 20:47:34 +0800 Subject: [PATCH] feat: add pidfile management for daemon process tracking --- packages/daemon/src/__tests__/pidfile.test.ts | 59 +++++++++++++++++++ packages/daemon/src/pidfile.ts | 48 +++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 packages/daemon/src/__tests__/pidfile.test.ts create mode 100644 packages/daemon/src/pidfile.ts diff --git a/packages/daemon/src/__tests__/pidfile.test.ts b/packages/daemon/src/__tests__/pidfile.test.ts new file mode 100644 index 0000000..2ddaeef --- /dev/null +++ b/packages/daemon/src/__tests__/pidfile.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import os from 'node:os'; +import fs from 'node:fs'; +import path from 'node:path'; +import { writePidfile, readPidfile, cleanPidfile, isProcessAlive } from '../pidfile.js'; + +describe('pidfile management', () => { + let testDir: string; + + beforeEach(() => { + testDir = path.join(os.tmpdir(), `visionl-pidfile-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + // clean up before in case previous run left files + try { fs.rmSync(testDir, { recursive: true, force: true }); } catch { /* ignore */ } + fs.mkdirSync(testDir, { recursive: true }); + }); + + it('writePidfile writes pid and port files, readPidfile reads them back', () => { + writePidfile(12345, 9527, testDir); + + const result = readPidfile(testDir); + expect(result).toEqual({ pid: 12345, port: 9527 }); + }); + + it('readPidfile returns null when pidfile does not exist', () => { + const result = readPidfile(testDir); + expect(result).toBeNull(); + }); + + it('readPidfile returns null when pidfile has invalid content', () => { + writePidfile(12345, 9527, testDir); + // overwrite with invalid data + fs.writeFileSync(path.join(testDir, 'daemon.pid'), 'not-a-number'); + + const result = readPidfile(testDir); + expect(result).toBeNull(); + }); + + it('cleanPidfile removes both pid and port files', () => { + writePidfile(12345, 9527, testDir); + + cleanPidfile(testDir); + + expect(fs.existsSync(path.join(testDir, 'daemon.pid'))).toBe(false); + expect(fs.existsSync(path.join(testDir, 'daemon.port'))).toBe(false); + }); + + it('cleanPidfile is safe when files do not exist', () => { + // should not throw + expect(() => cleanPidfile(testDir)).not.toThrow(); + }); + + it('isProcessAlive returns true for the current process', () => { + expect(isProcessAlive(process.pid)).toBe(true); + }); + + it('isProcessAlive returns false for a non-existent PID', () => { + expect(isProcessAlive(99999999)).toBe(false); + }); +}); diff --git a/packages/daemon/src/pidfile.ts b/packages/daemon/src/pidfile.ts new file mode 100644 index 0000000..b0f739c --- /dev/null +++ b/packages/daemon/src/pidfile.ts @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +const DEFAULT_DIR = path.join(os.homedir(), '.visionl'); + +function ensureDir(dir: string): void { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +export function writePidfile(pid: number, port: number, dir: string = DEFAULT_DIR): void { + ensureDir(dir); + fs.writeFileSync(path.join(dir, 'daemon.pid'), String(pid)); + fs.writeFileSync(path.join(dir, 'daemon.port'), String(port)); +} + +export function readPidfile(dir: string = DEFAULT_DIR): { pid: number; port: number } | null { + const pidPath = path.join(dir, 'daemon.pid'); + const portPath = path.join(dir, 'daemon.port'); + try { + const pid = parseInt(fs.readFileSync(pidPath, 'utf-8'), 10); + const port = parseInt(fs.readFileSync(portPath, 'utf-8'), 10); + if (isNaN(pid) || isNaN(port)) return null; + return { pid, port }; + } catch { + return null; + } +} + +export function cleanPidfile(dir: string = DEFAULT_DIR): void { + try { + fs.unlinkSync(path.join(dir, 'daemon.pid')); + fs.unlinkSync(path.join(dir, 'daemon.port')); + } catch { + // Ignore if files don't exist + } +} + +export function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}