|
|
#!/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 |
|
|
|