@wendongfly/zihi 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/daemon.js CHANGED
@@ -1,23 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * zihi 守护进程:写入 PID 并启动 server
3
+ * zihi 守护进程:标记为后台模式并启动 server
4
+ * PID 文件的写入/清理由 server 自己负责(写的是完整 JSON 元信息)。
4
5
  */
5
- import { writeFileSync, unlinkSync } from 'fs';
6
- import { join, dirname } from 'path';
7
- import { fileURLToPath } from 'url';
8
- import { homedir } from 'os';
9
-
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const pidFile = join(homedir(), '.zihi', 'daemon.pid');
12
-
13
- // 写入自身 PID
14
- writeFileSync(pidFile, String(process.pid));
15
-
16
- // 退出时清理 PID 文件
17
- function cleanup() { try { unlinkSync(pidFile); } catch {} }
18
- process.on('exit', cleanup);
19
- process.on('SIGTERM', () => process.exit(0));
20
- process.on('SIGINT', () => process.exit(0));
21
-
22
- // 启动 server
6
+ process.env.ZIHI_DAEMON = '1';
23
7
  await import('../dist/index.js');
package/bin/zihi.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'child_process';
2
+ import { spawn, execSync } from 'child_process';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
5
  import { mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
@@ -30,31 +30,142 @@ const logFile = join(configDir, 'daemon.log');
30
30
 
31
31
  // ── 工具函数 ──────────────────────────────────────────────
32
32
 
33
- function readPid() {
34
- try { return parseInt(readFileSync(pidFile, 'utf8').trim(), 10); }
35
- catch { return null; }
33
+ /** 读取 pidfile,兼容 JSON(新格式)和纯 PID 数字(旧格式)。 */
34
+ function readPidInfo() {
35
+ try {
36
+ const raw = readFileSync(pidFile, 'utf8').trim();
37
+ if (!raw) return null;
38
+ if (raw.startsWith('{')) return JSON.parse(raw);
39
+ const pid = parseInt(raw, 10);
40
+ return pid ? { pid } : null;
41
+ } catch { return null; }
36
42
  }
37
43
 
38
- function isRunning(pid) {
44
+ function isPidAlive(pid) {
39
45
  try { process.kill(pid, 0); return true; }
40
46
  catch { return false; }
41
47
  }
42
48
 
43
- function stopDaemon() {
44
- const pid = readPid();
45
- if (!pid) { console.log('[zihi] 没有正在运行的后台进程'); return false; }
46
- if (!isRunning(pid)) {
49
+ /** 查进程命令行是否含 zihi 标识,防 PID 复用导致误判成别的 node 进程。 */
50
+ function pidCmdlineLooksLikeZihi(pid) {
51
+ try {
52
+ if (process.platform === 'win32') {
53
+ const out = execSync(
54
+ `wmic process where "ProcessId=${pid}" get CommandLine /format:list`,
55
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }
56
+ );
57
+ return /zihi/i.test(out) && /(daemon\.js|dist[\\/]index\.js)/i.test(out);
58
+ } else {
59
+ const out = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
60
+ return /zihi/i.test(out) && /(daemon\.js|dist\/index\.js)/i.test(out);
61
+ }
62
+ } catch { return false; }
63
+ }
64
+
65
+ /** 向运行中的实例发健康探针:确认签名为 zihi 且 pid 匹配。 */
66
+ async function probeZihiPing(port, timeoutMs = 1500) {
67
+ if (!port) return null;
68
+ try {
69
+ const ctrl = new AbortController();
70
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
71
+ const resp = await fetch(`http://127.0.0.1:${port}/api/zihi-ping`, { signal: ctrl.signal });
72
+ clearTimeout(t);
73
+ if (!resp.ok) return null;
74
+ const data = await resp.json();
75
+ return data && data.zihi === true ? data : null;
76
+ } catch { return null; }
77
+ }
78
+
79
+ /**
80
+ * 校验当前 pidfile 指向的是否确实是一个活着的 zihi 实例。
81
+ * 两层校验:HTTP 探针 为主,cmdline 检查 为辅。
82
+ */
83
+ async function verifyRunning() {
84
+ const info = readPidInfo();
85
+ if (!info || !info.pid) return null;
86
+ if (!isPidAlive(info.pid)) return null;
87
+
88
+ // 主校验:健康探针
89
+ const ping = await probeZihiPing(info.port);
90
+ if (ping && ping.pid === info.pid) {
91
+ return { ...info, ...ping, verifiedBy: 'http' };
92
+ }
93
+
94
+ // 辅校验:命令行关键字
95
+ if (pidCmdlineLooksLikeZihi(info.pid)) {
96
+ return { ...info, verifiedBy: 'cmdline' };
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ function formatUptime(startedAt) {
103
+ if (!startedAt) return '未知';
104
+ const ms = Date.now() - startedAt;
105
+ const s = Math.floor(ms / 1000);
106
+ if (s < 60) return `${s}s`;
107
+ const m = Math.floor(s / 60);
108
+ if (m < 60) return `${m}m${s % 60}s`;
109
+ const h = Math.floor(m / 60);
110
+ return `${h}h${m % 60}m`;
111
+ }
112
+
113
+ function printRunningInfo(info, opts = {}) {
114
+ const started = info.startedAt ? new Date(info.startedAt).toLocaleString() : '未知';
115
+ console.log(`[zihi] 服务已在运行,跳过启动`);
116
+ console.log(` PID: ${info.pid}`);
117
+ if (info.port) console.log(` 端口: ${info.port}`);
118
+ if (info.version) console.log(` 版本: v${info.version}`);
119
+ if (info.cwd) console.log(` 工作目录: ${info.cwd}`);
120
+ if (info.execPath) console.log(` Node: ${info.execPath}`);
121
+ console.log(` 启动时间: ${started} (运行 ${formatUptime(info.startedAt)})`);
122
+ console.log(` 校验方式: ${info.verifiedBy === 'http' ? 'HTTP 探针' : '命令行匹配'}`);
123
+ if (info.port) console.log(` 管理界面: http://localhost:${info.port}/admin`);
124
+
125
+ const n = opts.logLines ?? 20;
126
+ if (n > 0) {
127
+ try {
128
+ const content = readFileSync(logFile, 'utf8');
129
+ const lines = content.split('\n');
130
+ console.log(`\n ── 最近 ${n} 行日志 ──`);
131
+ console.log(lines.slice(-n).join('\n'));
132
+ } catch {}
133
+ }
134
+ }
135
+
136
+ async function stopDaemon() {
137
+ const info = readPidInfo();
138
+ if (!info || !info.pid) { console.log('[zihi] 没有正在运行的后台进程'); return false; }
139
+ if (!isPidAlive(info.pid)) {
47
140
  try { unlinkSync(pidFile); } catch {}
48
141
  console.log('[zihi] 进程已不存在,已清理 PID 文件');
49
142
  return false;
50
143
  }
51
- process.kill(pid, 'SIGTERM');
144
+ // 额外校验:确认 PID 确实是 zihi,避免 PID 复用后误杀别人
145
+ const verified = await verifyRunning();
146
+ if (!verified) {
147
+ try { unlinkSync(pidFile); } catch {}
148
+ console.log(`[zihi] PID ${info.pid} 存在但不是 zihi 进程,已清理陈旧 PID 文件`);
149
+ return false;
150
+ }
151
+ process.kill(info.pid, 'SIGTERM');
52
152
  try { unlinkSync(pidFile); } catch {}
53
- console.log(`[zihi] 已停止后台进程 (PID ${pid})`);
153
+ console.log(`[zihi] 已停止后台进程 (PID ${info.pid})`);
54
154
  return true;
55
155
  }
56
156
 
57
- function startDaemon() {
157
+ async function startDaemon() {
158
+ // 启动前唯一性校验
159
+ const running = await verifyRunning();
160
+ if (running) {
161
+ printRunningInfo(running);
162
+ process.exit(0);
163
+ }
164
+ // pidfile 存在但校验失败 → 清理陈旧文件
165
+ if (readPidInfo()) {
166
+ try { unlinkSync(pidFile); } catch {}
167
+ }
168
+
58
169
  mkdirSync(configDir, { recursive: true });
59
170
  const out = openSync(logFile, 'a');
60
171
  // 启动守护进程(daemon.js),由它管理 server 子进程
@@ -103,21 +214,28 @@ if (cmd === 'attach') {
103
214
  await import('../dist/attach.js');
104
215
 
105
216
  } else if (cmd === 'stop') {
106
- stopDaemon();
217
+ await stopDaemon();
107
218
 
108
219
  } else if (cmd === 'restart') {
109
- stopDaemon();
220
+ await stopDaemon();
110
221
  await new Promise(r => setTimeout(r, 500));
111
- startDaemon();
222
+ await startDaemon();
112
223
  process.exit(0);
113
224
 
114
225
  } else if (cmd === 'status') {
115
226
  const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
116
- const pid = readPid();
117
- if (pid && isRunning(pid)) {
118
- console.log(`[zihi] v${pkgVersion} 运行中 (PID ${pid})`);
227
+ const running = await verifyRunning();
228
+ if (running) {
229
+ console.log(`[zihi] v${pkgVersion} 运行中`);
230
+ printRunningInfo(running, { logLines: 0 });
119
231
  } else {
120
- console.log(`[zihi] v${pkgVersion} 未运行`);
232
+ const stale = readPidInfo();
233
+ if (stale) {
234
+ try { unlinkSync(pidFile); } catch {}
235
+ console.log(`[zihi] v${pkgVersion} 未运行(已清理陈旧 PID 文件)`);
236
+ } else {
237
+ console.log(`[zihi] v${pkgVersion} 未运行`);
238
+ }
121
239
  }
122
240
 
123
241
  } else if (cmd === 'log' || cmd === 'logs') {
@@ -594,10 +712,17 @@ SSH 开发:
594
712
  `);
595
713
 
596
714
  } else if (cmd === '-d' || cmd === '--daemon' || cmd === 'daemon') {
597
- startDaemon();
598
- process.exit(0);
715
+ await startDaemon();
716
+ // startDaemon 内部会在检测到已运行时 exit(0)
717
+ // 正常启动路径由 poll 定时器结束进程。
599
718
 
600
719
  } else {
601
- // 默认 / 'start':前台运行
720
+ // 默认 / 'start':前台运行前也要做唯一性校验
721
+ const running = await verifyRunning();
722
+ if (running) {
723
+ printRunningInfo(running);
724
+ process.exit(0);
725
+ }
726
+ if (readPidInfo()) { try { unlinkSync(pidFile); } catch {} }
602
727
  await import('../dist/index.js');
603
728
  }