mslxdff 0.1.209 → 0.1.210

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.209",
3
+ "version": "0.1.210",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/daemon.js CHANGED
@@ -102,12 +102,26 @@ export function isPidAlive(pid) {
102
102
  }
103
103
  }
104
104
 
105
- export function stopDaemon({ reason = "stop", kill = process.kill, taskkill = spawnSync } = {}) {
105
+ export function stopDaemon({ reason = "stop", kill = process.kill, taskkill = spawnSync, isAlive = isPidAlive } = {}) {
106
106
  const pid = readPid();
107
107
  if (!pid) return { stopped: false, reason: "no pid file" };
108
+ // 自杀护栏(最高优先级):pid 文件里写着**本进程**时,杀它就是杀自己。
109
+ // 这不是假想情形 —— daemon 自己 `writePid(process.pid)`(server-lifecycle.js),而 `enableAutostart()`
110
+ // 会在 systemd 托管下被 daemon 自己调起(server-lifecycle 的 autostart 自检块):systemd **系统** unit
111
+ // 的 daemon 去查 **user** unit 必然判「未启用」⇒ 走进 enableAutostart ⇒ stopDaemon ⇒ SIGTERM 自己
112
+ // ⇒ 优雅关停 exit 0 ⇒ systemd Restart=always 拉起 ⇒ 每 2.5 秒一轮的无限重启
113
+ //(真机 2026-10-19:0.1.209 + 系统 unit,每进程活 2~3s 被 SIGTERM;0.1.160 不炸只因当时
114
+ // `enableAutostart` 里 import 路径写错、这段从未执行过,2026-10-17 c7bf8af 修正路径后暴露)。
115
+ if (pid === process.pid) {
116
+ try {
117
+ appendFileSync(logFile(), `[lifecycle] stopDaemon refused: pid file points at self (pid=${pid}, reason=${reason})\n`);
118
+ } catch {}
119
+ return { stopped: false, pid, reason: "self — pid 文件写的就是本进程,拒绝自杀" };
120
+ }
121
+
108
122
  // 杀者牌(验尸官的另一半证据):动手前写、自带死者身份。只给在世者留牌——给尸体留牌
109
123
  // 会让下次验尸把"早已静默死亡"误判成"被本次杀死"。
110
- const victimAlive = isPidAlive(pid);
124
+ const victimAlive = isAlive(pid);
111
125
  // 杀者留痕(写 daemon.log):Windows 的 SIGTERM 是 TerminateProcess 强杀,被杀的 daemon
112
126
  // 在 JS 层收不到任何事件(无法自记),故由"杀者"记录调用方 pid——
113
127
  // 没有这行 = 非我方所杀(外部 taskkill/任务管理器/崩溃),配合最后一条 heartbeat 定位死亡时刻。
@@ -92,6 +92,17 @@ export function isDaemonArgv(argv) {
92
92
  return list.includes("--daemon") && list.some((a) => /(^|[\\/])mslxdff(\.js)?$/i.test(String(a)));
93
93
  }
94
94
 
95
+ /**
96
+ * 是否跑在 systemd 托管下(system 或 user unit 都算)。
97
+ * 判据:systemd 给被管服务注入 `INVOCATION_ID`(每个 invocation 唯一)与 `JOURNAL_STREAM`(stdout 连接信息)。
98
+ * 为什么要问:托管时 supervisor 已保证单实例与重启策略,daemon 再自己「杀旧进程/杀端口占用者」是职责重叠,
99
+ * 会与 systemd 的拉起时序打架成死循环(真机 2026-10-19:0.1.209 + 系统 unit 每 2.5s 自杀一轮)。
100
+ * @returns {boolean}
101
+ */
102
+ export function underSystemd(env = process.env) {
103
+ return Boolean(env.INVOCATION_ID || env.JOURNAL_STREAM);
104
+ }
105
+
95
106
  /** 「已运行 6h12m」这种人话时长(取证日志用)。 */
96
107
  export function fmtAlive(sec) {
97
108
  if (!Number.isFinite(sec)) return "运行时长未知";
@@ -5,6 +5,7 @@ import { runHook } from "../plugins.js";
5
5
  import { effectiveHost, maxHopsValue } from "../cli/policy.js";
6
6
  import { fmtEvent } from "../cli/format.js";
7
7
  import { writePid } from "../daemon.js";
8
+ import { underSystemd } from "./proc-info.js";
8
9
 
9
10
  /**
10
11
  * 服务生命周期 — router 装配 → server 创建/ready-EADDRINUSE 自愈 →
@@ -66,6 +67,13 @@ export async function startServerLifecycle({ VERSION, token, created, upstream,
66
67
  const msg = String(err?.message || err);
67
68
  const code = err?.code || "";
68
69
  if (code === "EADDRINUSE" || msg.includes("EADDRINUSE")) {
70
+ // systemd 托管下不许杀端口占用者:supervisor 保证单实例,去杀「占用者」是越权,且会与
71
+ // systemd 的拉起时序互杀成循环(报告的另一半)。非零退出,让 systemd 按 Restart/RestartSec
72
+ // 退避重试——这才是托管语义下正确的收口。
73
+ if (underSystemd()) {
74
+ console.error(`[lifecycle] EADDRINUSE on port ${resolvePort()} under systemd — exit(1), let systemd backoff-retry(不杀任何进程)`);
75
+ process.exit(1);
76
+ }
69
77
  console.log(`[lifecycle] EADDRINUSE on port ${resolvePort()} — freeing stale holder and retrying...`);
70
78
  try {
71
79
  const { execFile } = await import("node:child_process");
@@ -166,7 +174,13 @@ export async function startServerLifecycle({ VERSION, token, created, upstream,
166
174
  } catch {}
167
175
 
168
176
  // best-effort: ensure autostart on Linux (so daemon survives reboot/SSH disconnect without manual cmd)
169
- if (process.platform === "linux" && !process.env.MSLXDFF_NO_AUTOSTART) {
177
+ // **systemd 托管下必须跳过**(2026-10-19 真机无限重启的根因):
178
+ // - 托管时 supervisor 已经保证单实例+开机自启,这里再 enable 一次多余;
179
+ // - 本块查的是 **user** unit(`~/.config/systemd/user/mslxdff.service`),而 systemd **系统**
180
+ // unit 托管的 daemon 查它必然得 enabled=false ⇒ 走 enableAutostart() ⇒ linuxEnable() ⇒ stopDaemon()。
181
+ // 那条路径里 pid 文件写的就是**本进程**,过去等于每 2.5s 自杀一次(见 daemon.js 的自杀护栏)。
182
+ // systemd 会给被管进程注入 INVOCATION_ID / JOURNAL_STREAM,判据可靠、无需用户配置。
183
+ if (process.platform === "linux" && !process.env.MSLXDFF_NO_AUTOSTART && !underSystemd()) {
170
184
  setTimeout(async () => {
171
185
  try {
172
186
  const { getAutostartStatus, enableAutostart } = await import("../autostart.js");