runtime-state.mjs 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import { createRequire } from 'node:module';
  4. import { PACKAGE_ROOT } from './config-loader.mjs';
  5. const require = createRequire(import.meta.url);
  6. const { outputsRoot } = require(path.join(PACKAGE_ROOT, 'mcp', 'src', 'core', 'output-paths.js'));
  7. const SECRET_KEY = /(token|secret|private.?key|authorization|credential|password)/i;
  8. function redactState(value) {
  9. if (Array.isArray(value)) return value.map(redactState);
  10. if (!value || typeof value !== 'object') return value;
  11. const output = {};
  12. for (const [key, item] of Object.entries(value)) {
  13. if (SECRET_KEY.test(key)) continue;
  14. output[key] = redactState(item);
  15. }
  16. return output;
  17. }
  18. export function runtimeStatePath(customPath = '') {
  19. return customPath || path.join(outputsRoot(), 'runtime', 'qiwei-runtime.json');
  20. }
  21. export function runtimeStopRequestPath(customPath = '') {
  22. return runtimeStatePath(customPath).replace(/\.json$/i, '.stop.json');
  23. }
  24. export function readRuntimeState(customPath = '') {
  25. const filePath = runtimeStatePath(customPath);
  26. try {
  27. return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
  28. } catch {
  29. return {};
  30. }
  31. }
  32. export function writeRuntimeState(state, customPath = '') {
  33. const filePath = runtimeStatePath(customPath);
  34. fs.mkdirSync(path.dirname(filePath), { recursive: true });
  35. const safe = redactState({ ...state, updatedAt: new Date().toISOString() });
  36. const temporary = `${filePath}.${process.pid}.tmp`;
  37. fs.writeFileSync(temporary, `${JSON.stringify(safe, null, 2)}\n`, 'utf8');
  38. fs.renameSync(temporary, filePath);
  39. return safe;
  40. }
  41. export function isProcessAlive(pid) {
  42. const numericPid = Number(pid);
  43. if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
  44. try {
  45. process.kill(numericPid, 0);
  46. return true;
  47. } catch {
  48. return false;
  49. }
  50. }
  51. export function assertRuntimeAvailable(customPath = '') {
  52. const current = readRuntimeState(customPath);
  53. if (current.pid && current.pid !== process.pid && current.status !== 'stopped' && isProcessAlive(current.pid)) {
  54. throw new Error(`Qiwei runtime is already running (pid=${current.pid}, mode=${current.mode || 'unknown'}).`);
  55. }
  56. return current;
  57. }
  58. export function readRuntimeStopRequest(customPath = '') {
  59. try {
  60. return JSON.parse(fs.readFileSync(runtimeStopRequestPath(customPath), 'utf8').replace(/^\uFEFF/, ''));
  61. } catch {
  62. return {};
  63. }
  64. }
  65. export function clearRuntimeStopRequest(customPath = '') {
  66. try { fs.unlinkSync(runtimeStopRequestPath(customPath)); } catch {}
  67. }
  68. export function requestRuntimeStop(customPath = '') {
  69. const current = readRuntimeState(customPath);
  70. const filePath = runtimeStopRequestPath(customPath);
  71. fs.mkdirSync(path.dirname(filePath), { recursive: true });
  72. fs.writeFileSync(filePath, `${JSON.stringify({
  73. targetPid: Number(current.pid) || null,
  74. requestedAt: new Date().toISOString(),
  75. }, null, 2)}\n`, 'utf8');
  76. return { requested: true, pid: Number(current.pid) || null };
  77. }
  78. export function stopRuntimeProcess(customPath = '') {
  79. const current = readRuntimeState(customPath);
  80. if (!isProcessAlive(current.pid)) {
  81. writeRuntimeState({ ...current, status: 'stopped', stoppedAt: new Date().toISOString() }, customPath);
  82. return { stopped: false, reason: 'not-running', pid: current.pid || null };
  83. }
  84. return requestRuntimeStop(customPath);
  85. }
  86. export { redactState };