UNPKG

@claude-vector/cli

Version:

CLI for Claude-integrated vector search

562 lines (464 loc) 15.2 kB
/** * ProcessController - プロセス管理・状態制御システム * * 機能: * - PIDファイル管理(重複実行防止) * - 状態ファイル管理(実行状況永続化) * - グレースフルシャットダウン * - プロセス存在確認 * - 信号ハンドリング * - マルチプロセス制御 */ import { EventEmitter } from 'events'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import { spawn } from 'child_process'; /** * プロセス制御のデフォルト設定 */ const PROCESS_DEFAULTS = { // ファイル名設定 pidFile: '.claude-watch.pid', statusFile: '.claude-watch.status', lockFile: '.claude-watch.lock', // タイムアウト設定 shutdownTimeout: 30000, // 30秒でタイムアウト startupTimeout: 10000, // 10秒で起動タイムアウト healthCheckInterval: 5000, // 5秒間隔でヘルスチェック // プロセス設定 allowMultiple: false, // 重複実行を許可しない autoRestart: false, // 自動再起動しない maxRestarts: 3, // 最大再起動回数 // ログ設定 logStdout: true, // 標準出力をログ logStderr: true, // 標準エラーをログ // デーモン設定 daemonMode: false, // デフォルトはフォアグラウンド detached: false // プロセス分離 }; export class ProcessController extends EventEmitter { constructor(config = {}) { super(); this.config = { ...PROCESS_DEFAULTS, ...config }; this.projectRoot = null; this.isInitialized = false; // プロセス状態 this.state = { pid: process.pid, startTime: new Date(), status: 'initializing', parentPid: process.ppid, nodeVersion: process.version, platform: process.platform, arch: process.arch, cwd: process.cwd(), // 統計情報 restartCount: 0, lastRestart: null, lastError: null, // ヘルスチェック lastHeartbeat: new Date(), isHealthy: true }; // ファイルパス this.filePaths = {}; // タイマー this.healthCheckTimer = null; this.shutdownTimer = null; // フラグ this.isShuttingDown = false; this.isLocked = false; this.setupSignalHandlers(); } /** * 初期化 */ async initialize(projectRoot) { if (this.isInitialized) { return; } try { this.projectRoot = projectRoot; // ファイルパスの設定 this.filePaths = { pidFile: path.join(projectRoot, this.config.pidFile), statusFile: path.join(projectRoot, this.config.statusFile), lockFile: path.join(projectRoot, this.config.lockFile) }; // 既存プロセスの確認 await this.checkExistingProcess(); // ロックファイルの作成 await this.acquireLock(); // PIDファイルの作成 await this.createPidFile(); // 状態ファイルの作成 await this.createStatusFile(); // ヘルスチェック開始 this.startHealthCheck(); this.state.status = 'running'; this.isInitialized = true; this.emit('initialized', { pid: this.state.pid, projectRoot, startTime: this.state.startTime }); } catch (error) { this.state.status = 'error'; this.state.lastError = error.message; throw error; } } /** * 既存プロセス確認 */ async checkExistingProcess() { const pidFile = this.filePaths.pidFile; if (!fsSync.existsSync(pidFile)) { return; // PIDファイルがない場合は問題なし } try { const pidContent = await fs.readFile(pidFile, 'utf-8'); const pid = parseInt(pidContent.trim()); if (isNaN(pid)) { // 無効なPIDファイルは削除 await fs.unlink(pidFile); return; } // プロセス存在確認 const isRunning = await this.isProcessRunning(pid); if (isRunning) { if (!this.config.allowMultiple) { throw new Error(`Watch process already running (PID: ${pid}). Use 'ccvector watch stop' to stop it.`); } else { console.warn(`⚠️ Another watch process is running (PID: ${pid})`); } } else { // 古いPIDファイルを削除 await fs.unlink(pidFile); console.log('🧹 Cleaned up stale PID file'); } } catch (error) { if (error.code === 'ENOENT') { return; // ファイルが存在しない } throw error; } } /** * プロセス存在確認 */ async isProcessRunning(pid) { try { // kill(pid, 0) はプロセス存在確認に使用(実際にはkillしない) process.kill(pid, 0); return true; } catch (error) { if (error.code === 'ESRCH') { return false; // プロセスが存在しない } else if (error.code === 'EPERM') { return true; // 権限がないが存在する } else { throw error; } } } /** * ロック取得 */ async acquireLock() { const lockFile = this.filePaths.lockFile; try { // 排他制御でロックファイル作成 const lockData = { pid: this.state.pid, startTime: this.state.startTime.toISOString(), nodeVersion: this.state.nodeVersion, projectRoot: this.projectRoot }; // O_EXCL フラグで排他作成(既存ファイルがあると失敗) await fs.writeFile(lockFile, JSON.stringify(lockData, null, 2), { flag: 'wx' }); this.isLocked = true; } catch (error) { if (error.code === 'EEXIST') { // ロックファイルが既に存在 const existingLock = JSON.parse(await fs.readFile(lockFile, 'utf-8')); const isRunning = await this.isProcessRunning(existingLock.pid); if (isRunning) { throw new Error(`Another process has acquired lock (PID: ${existingLock.pid})`); } else { // 古いロックファイルを削除して再試行 await fs.unlink(lockFile); await this.acquireLock(); } } else { throw error; } } } /** * PIDファイル作成 */ async createPidFile() { const pidFile = this.filePaths.pidFile; await fs.writeFile(pidFile, this.state.pid.toString()); } /** * 状態ファイル作成 */ async createStatusFile() { await this.updateStatusFile(); } /** * 状態ファイル更新 */ async updateStatusFile() { const statusFile = this.filePaths.statusFile; const statusData = { ...this.state, lastUpdate: new Date().toISOString(), uptime: Date.now() - this.state.startTime.getTime(), memoryUsage: process.memoryUsage(), cpuUsage: process.cpuUsage() }; try { await fs.writeFile(statusFile, JSON.stringify(statusData, null, 2)); } catch (error) { console.warn('⚠️ Failed to update status file:', error.message); } } /** * ヘルスチェック開始 */ startHealthCheck() { if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); } this.healthCheckTimer = setInterval(async () => { try { await this.performHealthCheck(); } catch (error) { console.error('❌ Health check failed:', error.message); this.state.isHealthy = false; this.state.lastError = error.message; } }, this.config.healthCheckInterval); } /** * ヘルスチェック実行 */ async performHealthCheck() { // メモリ使用量チェック const memUsage = process.memoryUsage(); const memMB = Math.round(memUsage.heapUsed / 1024 / 1024); if (memMB > 1000) { // 1GB以上 console.warn(`⚠️ High memory usage: ${memMB}MB`); } // ハートビート更新 this.state.lastHeartbeat = new Date(); this.state.isHealthy = true; // 状態ファイル更新 await this.updateStatusFile(); this.emit('heartbeat', { timestamp: this.state.lastHeartbeat, memoryMB: memMB, uptime: Date.now() - this.state.startTime.getTime() }); } /** * 信号ハンドラ設定 */ setupSignalHandlers() { // SIGINT (Ctrl+C) process.on('SIGINT', async () => { console.log('\n🛑 Received SIGINT, shutting down gracefully...'); await this.shutdown('SIGINT'); }); // SIGTERM (kill) process.on('SIGTERM', async () => { console.log('\n🛑 Received SIGTERM, shutting down gracefully...'); await this.shutdown('SIGTERM'); }); // SIGHUP (reload) process.on('SIGHUP', () => { console.log('\n🔄 Received SIGHUP, reloading configuration...'); this.emit('reload'); }); // プロセス例外ハンドリング process.on('uncaughtException', async (error) => { console.error('❌ Uncaught exception:', error); this.state.lastError = error.message; await this.shutdown('uncaughtException'); }); process.on('unhandledRejection', async (reason) => { console.error('❌ Unhandled rejection:', reason); this.state.lastError = reason?.message || String(reason); await this.shutdown('unhandledRejection'); }); } /** * グレースフルシャットダウン */ async shutdown(reason = 'manual') { if (this.isShuttingDown) { console.log('⏳ Shutdown already in progress...'); return; } this.isShuttingDown = true; this.state.status = 'shutting_down'; console.log(`🛑 Starting graceful shutdown (reason: ${reason})...`); try { // シャットダウンタイマー設定 this.shutdownTimer = setTimeout(() => { console.log('⚠️ Graceful shutdown timeout, forcing exit...'); process.exit(1); }, this.config.shutdownTimeout); // シャットダウンイベント発行 this.emit('shutdown-start', { reason, timestamp: new Date() }); // ヘルスチェック停止 if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); this.healthCheckTimer = null; } // 状態ファイル最終更新 this.state.status = 'stopped'; await this.updateStatusFile(); // クリーンアップ await this.cleanup(); console.log('✅ Graceful shutdown completed'); // シャットダウン完了イベント this.emit('shutdown-complete', { reason, duration: Date.now() - this.state.startTime.getTime() }); // タイマークリア if (this.shutdownTimer) { clearTimeout(this.shutdownTimer); this.shutdownTimer = null; } // プロセス終了 process.exit(0); } catch (error) { console.error('❌ Error during shutdown:', error.message); process.exit(1); } } /** * クリーンアップ */ async cleanup() { const cleanupTasks = []; // PIDファイル削除 if (this.filePaths.pidFile && fsSync.existsSync(this.filePaths.pidFile)) { cleanupTasks.push(fs.unlink(this.filePaths.pidFile)); } // ロックファイル削除 if (this.isLocked && this.filePaths.lockFile && fsSync.existsSync(this.filePaths.lockFile)) { cleanupTasks.push(fs.unlink(this.filePaths.lockFile)); } // 状態ファイルは保持(デバッグ用) try { await Promise.all(cleanupTasks); console.log('🧹 Cleanup completed'); } catch (error) { console.warn('⚠️ Cleanup error:', error.message); } } /** * 状態取得 */ async getStatus() { // 現在の状態を返す const currentStatus = { ...this.state, uptime: Date.now() - this.state.startTime.getTime(), isInitialized: this.isInitialized, isShuttingDown: this.isShuttingDown, isLocked: this.isLocked, memoryUsage: process.memoryUsage(), filePaths: this.filePaths }; return currentStatus; } /** * 実行中プロセス一覧取得 */ static async getRunningProcesses(projectRoot) { const pidFile = path.join(projectRoot, PROCESS_DEFAULTS.pidFile); const statusFile = path.join(projectRoot, PROCESS_DEFAULTS.statusFile); const processes = []; if (fsSync.existsSync(pidFile) && fsSync.existsSync(statusFile)) { try { const pidContent = await fs.readFile(pidFile, 'utf-8'); const pid = parseInt(pidContent.trim()); const statusContent = await fs.readFile(statusFile, 'utf-8'); const status = JSON.parse(statusContent); const controller = new ProcessController(); const isRunning = await controller.isProcessRunning(pid); if (isRunning) { processes.push({ pid, status, isRunning: true }); } } catch (error) { // ファイル読み込みエラーは無視 } } return processes; } /** * プロセス停止 */ static async stopProcess(projectRoot, force = false) { const pidFile = path.join(projectRoot, PROCESS_DEFAULTS.pidFile); if (!fsSync.existsSync(pidFile)) { throw new Error('No running watch process found'); } try { const pidContent = await fs.readFile(pidFile, 'utf-8'); const pid = parseInt(pidContent.trim()); const controller = new ProcessController(); const isRunning = await controller.isProcessRunning(pid); if (!isRunning) { // 古いPIDファイルを削除 await fs.unlink(pidFile); throw new Error('Process is not running (cleaned up stale PID file)'); } // プロセスにシグナル送信 const signal = force ? 'SIGKILL' : 'SIGTERM'; process.kill(pid, signal); // グレースフル停止の場合は少し待機 if (!force) { await new Promise(resolve => setTimeout(resolve, 2000)); // まだ動いているかチェック const stillRunning = await controller.isProcessRunning(pid); if (stillRunning) { console.log('⏳ Process still running, sending SIGKILL...'); process.kill(pid, 'SIGKILL'); } } return { pid, signal, success: true }; } catch (error) { if (error.code === 'ESRCH') { // プロセスが存在しない await fs.unlink(pidFile); throw new Error('Process not found (cleaned up stale PID file)'); } throw error; } } /** * デストラクタ */ async destroy() { await this.cleanup(); if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); } if (this.shutdownTimer) { clearTimeout(this.shutdownTimer); } } }