| #!/usr/bin/env node |
|
|
| const { spawn, exec } = require('child_process') |
| const fs = require('fs') |
| const path = require('path') |
| const process = require('process') |
|
|
| const PID_FILE = path.join(__dirname, '..', 'claude-relay-service.pid') |
| const LOG_FILE = path.join(__dirname, '..', 'logs', 'service.log') |
| const ERROR_LOG_FILE = path.join(__dirname, '..', 'logs', 'service-error.log') |
| const APP_FILE = path.join(__dirname, '..', 'src', 'app.js') |
|
|
| class ServiceManager { |
| constructor() { |
| this.ensureLogDir() |
| } |
|
|
| ensureLogDir() { |
| const logDir = path.dirname(LOG_FILE) |
| if (!fs.existsSync(logDir)) { |
| fs.mkdirSync(logDir, { recursive: true }) |
| } |
| } |
|
|
| getPid() { |
| try { |
| if (fs.existsSync(PID_FILE)) { |
| const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim()) |
| return pid |
| } |
| } catch (error) { |
| console.error('读取PID文件失败:', error.message) |
| } |
| return null |
| } |
|
|
| isProcessRunning(pid) { |
| try { |
| process.kill(pid, 0) |
| return true |
| } catch (error) { |
| return false |
| } |
| } |
|
|
| writePid(pid) { |
| try { |
| fs.writeFileSync(PID_FILE, pid.toString()) |
| console.log(`✅ PID ${pid} 已保存到 ${PID_FILE}`) |
| } catch (error) { |
| console.error('写入PID文件失败:', error.message) |
| } |
| } |
|
|
| removePidFile() { |
| try { |
| if (fs.existsSync(PID_FILE)) { |
| fs.unlinkSync(PID_FILE) |
| console.log('🗑️ 已清理PID文件') |
| } |
| } catch (error) { |
| console.error('清理PID文件失败:', error.message) |
| } |
| } |
|
|
| getStatus() { |
| const pid = this.getPid() |
| if (pid && this.isProcessRunning(pid)) { |
| return { running: true, pid } |
| } |
| return { running: false, pid: null } |
| } |
|
|
| start(daemon = false) { |
| const status = this.getStatus() |
| if (status.running) { |
| console.log(`⚠️ 服务已在运行中 (PID: ${status.pid})`) |
| return false |
| } |
|
|
| console.log('🚀 启动 Claude Relay Service...') |
|
|
| if (daemon) { |
| |
| const { exec: execChild } = require('child_process') |
|
|
| const command = `nohup node "${APP_FILE}" > "${LOG_FILE}" 2> "${ERROR_LOG_FILE}" & echo $!` |
|
|
| execChild(command, (error, stdout) => { |
| if (error) { |
| console.error('❌ 后台启动失败:', error.message) |
| return |
| } |
|
|
| const pid = parseInt(stdout.trim()) |
| if (pid && !isNaN(pid)) { |
| this.writePid(pid) |
| console.log(`🔄 服务已在后台启动 (PID: ${pid})`) |
| console.log(`📝 日志文件: ${LOG_FILE}`) |
| console.log(`❌ 错误日志: ${ERROR_LOG_FILE}`) |
| console.log('✅ 终端现在可以安全关闭') |
| } else { |
| console.error('❌ 无法获取进程ID') |
| } |
| }) |
|
|
| |
| setTimeout(() => { |
| process.exit(0) |
| }, 1000) |
| } else { |
| |
| const child = spawn('node', [APP_FILE], { |
| stdio: 'inherit' |
| }) |
|
|
| console.log(`🔄 服务已启动 (PID: ${child.pid})`) |
|
|
| this.writePid(child.pid) |
|
|
| |
| child.on('exit', (code, signal) => { |
| this.removePidFile() |
| if (code !== 0) { |
| console.log(`💥 进程退出 (代码: ${code}, 信号: ${signal})`) |
| } |
| }) |
|
|
| child.on('error', (error) => { |
| console.error('❌ 启动失败:', error.message) |
| this.removePidFile() |
| }) |
| } |
|
|
| return true |
| } |
|
|
| stop() { |
| const status = this.getStatus() |
| if (!status.running) { |
| console.log('⚠️ 服务未在运行') |
| this.removePidFile() |
| return false |
| } |
|
|
| console.log(`🛑 停止服务 (PID: ${status.pid})...`) |
|
|
| try { |
| |
| process.kill(status.pid, 'SIGTERM') |
|
|
| |
| let attempts = 0 |
| const maxAttempts = 30 |
|
|
| const checkExit = setInterval(() => { |
| attempts++ |
| if (!this.isProcessRunning(status.pid)) { |
| clearInterval(checkExit) |
| console.log('✅ 服务已停止') |
| this.removePidFile() |
| return |
| } |
|
|
| if (attempts >= maxAttempts) { |
| clearInterval(checkExit) |
| console.log('⚠️ 优雅关闭超时,强制终止进程...') |
| try { |
| process.kill(status.pid, 'SIGKILL') |
| console.log('✅ 服务已强制停止') |
| } catch (error) { |
| console.error('❌ 强制停止失败:', error.message) |
| } |
| this.removePidFile() |
| } |
| }, 1000) |
| } catch (error) { |
| console.error('❌ 停止服务失败:', error.message) |
| this.removePidFile() |
| return false |
| } |
|
|
| return true |
| } |
|
|
| restart(daemon = false) { |
| console.log('🔄 重启服务...') |
| this.stop() |
| |
| setTimeout(() => { |
| this.start(daemon) |
| }, 2000) |
|
|
| return true |
| } |
|
|
| status() { |
| const status = this.getStatus() |
| if (status.running) { |
| console.log(`✅ 服务正在运行 (PID: ${status.pid})`) |
|
|
| |
| exec(`ps -p ${status.pid} -o pid,ppid,pcpu,pmem,etime,cmd --no-headers`, (error, stdout) => { |
| if (!error && stdout.trim()) { |
| console.log('\n📊 进程信息:') |
| console.log('PID\tPPID\tCPU%\tMEM%\tTIME\t\tCOMMAND') |
| console.log(stdout.trim()) |
| } |
| }) |
| } else { |
| console.log('❌ 服务未运行') |
| } |
| return status.running |
| } |
|
|
| logs(lines = 50) { |
| console.log(`📖 最近 ${lines} 行日志:\n`) |
|
|
| exec(`tail -n ${lines} ${LOG_FILE}`, (error, stdout) => { |
| if (error) { |
| console.error('读取日志失败:', error.message) |
| return |
| } |
| console.log(stdout) |
| }) |
| } |
|
|
| help() { |
| console.log(` |
| 🔧 Claude Relay Service 进程管理器 |
| |
| 用法: npm run service <command> [options] |
| |
| 重要提示: |
| 如果要传递参数,请在npm run命令中使用 -- 分隔符 |
| npm run service <command> -- [options] |
| |
| 命令: |
| start [-d|--daemon] 启动服务 (-d: 后台运行) |
| stop 停止服务 |
| restart [-d|--daemon] 重启服务 (-d: 后台运行) |
| status 查看服务状态 |
| logs [lines] 查看日志 (默认50行) |
| help 显示帮助信息 |
| |
| 命令缩写: |
| s, start 启动服务 |
| r, restart 重启服务 |
| st, status 查看状态 |
| l, log, logs 查看日志 |
| halt, stop 停止服务 |
| h, help 显示帮助 |
| |
| 示例: |
| npm run service start # 前台启动 |
| npm run service -- start -d # 后台启动(正确方式) |
| npm run service:start:d # 后台启动(推荐快捷方式) |
| npm run service:daemon # 后台启动(推荐快捷方式) |
| npm run service stop # 停止服务 |
| npm run service -- restart -d # 后台重启(正确方式) |
| npm run service:restart:d # 后台重启(推荐快捷方式) |
| npm run service status # 查看状态 |
| npm run service logs # 查看日志 |
| npm run service -- logs 100 # 查看最近100行日志 |
| |
| 推荐的快捷方式(无需 -- 分隔符): |
| npm run service:start:d # 等同于 npm run service -- start -d |
| npm run service:restart:d # 等同于 npm run service -- restart -d |
| npm run service:daemon # 等同于 npm run service -- start -d |
| |
| 直接使用脚本(推荐): |
| node scripts/manage.js start -d # 后台启动 |
| node scripts/manage.js restart -d # 后台重启 |
| node scripts/manage.js status # 查看状态 |
| node scripts/manage.js logs 100 # 查看最近100行日志 |
| |
| 文件位置: |
| PID文件: ${PID_FILE} |
| 日志文件: ${LOG_FILE} |
| 错误日志: ${ERROR_LOG_FILE} |
| `) |
| } |
| } |
|
|
| |
| function main() { |
| const manager = new ServiceManager() |
| const args = process.argv.slice(2) |
| const command = args[0] |
| const isDaemon = args.includes('-d') || args.includes('--daemon') |
|
|
| switch (command) { |
| case 'start': |
| case 's': |
| manager.start(isDaemon) |
| break |
| case 'stop': |
| case 'halt': |
| manager.stop() |
| break |
| case 'restart': |
| case 'r': |
| manager.restart(isDaemon) |
| break |
| case 'status': |
| case 'st': |
| manager.status() |
| break |
| case 'logs': |
| case 'log': |
| case 'l': { |
| const lines = parseInt(args[1]) || 50 |
| manager.logs(lines) |
| break |
| } |
| case 'help': |
| case '--help': |
| case '-h': |
| case 'h': |
| manager.help() |
| break |
| default: |
| console.log('❌ 未知命令:', command) |
| manager.help() |
| process.exit(1) |
| } |
| } |
|
|
| if (require.main === module) { |
| main() |
| } |
|
|
| module.exports = ServiceManager |
|
|