@lark-codem/codem-cli 0.1.237 → 0.1.238

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": "@lark-codem/codem-cli",
3
- "version": "0.1.237",
3
+ "version": "0.1.238",
4
4
  "codemServiceManagementProtocol": 3,
5
5
  "codemProfileProtocol": 1,
6
6
  "type": "module",
@@ -29,16 +29,16 @@
29
29
  "access": "public"
30
30
  },
31
31
  "dependencies": {
32
- "@lark-codem/codem-core": "0.8.69",
32
+ "@lark-codem/codem-core": "0.8.70",
33
33
  "zod": "3.25.76"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@lark-codem/codem-cli-darwin-arm64": "0.1.237",
37
- "@lark-codem/codem-cli-darwin-x64": "0.1.237",
38
- "@lark-codem/codem-cli-linux-arm64": "0.1.237",
39
- "@lark-codem/codem-cli-linux-x64": "0.1.237",
40
- "@lark-codem/codem-cli-win32-arm64": "0.1.237",
41
- "@lark-codem/codem-cli-win32-x64": "0.1.237"
36
+ "@lark-codem/codem-cli-darwin-arm64": "0.1.238",
37
+ "@lark-codem/codem-cli-darwin-x64": "0.1.238",
38
+ "@lark-codem/codem-cli-linux-arm64": "0.1.238",
39
+ "@lark-codem/codem-cli-linux-x64": "0.1.238",
40
+ "@lark-codem/codem-cli-win32-arm64": "0.1.238",
41
+ "@lark-codem/codem-cli-win32-x64": "0.1.238"
42
42
  },
43
43
  "engines": {
44
44
  "node": ">=18"
@@ -61,6 +61,9 @@ import { debugLog } from "./debug.js";
61
61
  // ——PID 复用等边界最多让锁延迟释放,绝不以破坏互斥换可用性。回收者通过
62
62
  // `<lock>.reap/` 下的唯一候选做 Bakery 选举,把 token 复核与 unlink 串成单回收者
63
63
  // 临界区;候选绑定 PID,进程崩溃后可被后续回收者安全清理。
64
+ // - tmp 清扫:获取时顺带扫描同目录的 `<lock>.tmp-*` 兄弟,仅回收「可证明无主」
65
+ // (body 可读 + 本机 host + kill(pid,0) ESRCH)的残留——等待窗口内被 SIGKILL/Ctrl+C/
66
+ // 断电终止的进程留下的 tmp 由此收敛,body 读不出的一律不动(见 sweepAbandonedTmpFiles)。
64
67
  //
65
68
  // ⚠️ 明确不做「回收 0 字节锁」的自愈:新代码永不产生 0 字节锁,但 rolling upgrade 期间
66
69
  // 旧版进程仍可能瞬时留下 0 字节锁,且无法与旧版崩溃残骸区分——回收它就会误删活持有者。
@@ -157,6 +160,54 @@ async function readOwner(filePath) {
157
160
  }
158
161
  }
159
162
 
163
+ // 机会性清扫残留 tmp(Bug 7384601509):tmp 的生命周期原本只挂在「获取进程走完所有
164
+ // 出口」上——等待窗口内被 SIGINT/SIGTERM/SIGKILL/崩溃/断电终止的进程,其 tmp 因文件名
165
+ // 内嵌 randomUUID 永不复用而永久残留。这里在每次获取时顺带回收同目录的 `<lock>.tmp-*`
166
+ // 兄弟文件,判定与锁文件回收完全同一不变量:**可证明无主才删**(body 可读 + host 为
167
+ // 本机 + isPidAlive 判定 PID 不存在)。body 读不出的 tmp 一律不动(可能是正在写的活
168
+ // 等待者,误删会让其 fs.link 失败于 ENOENT 而不是 EEXIST,错误直接抛给调用方);异机
169
+ // host 不动(共享 HOME 场景不替别机做主)。清扫 best-effort:任何失败只留 debugLog,
170
+ // 绝不影响获取主流程。
171
+ async function sweepAbandonedTmpFiles(lockFile, { isPidAlive, ownTmp }) {
172
+ const dir = path.dirname(lockFile);
173
+ const prefix = `${path.basename(lockFile)}.tmp-`;
174
+ let entries;
175
+ try {
176
+ entries = await fs.readdir(dir, { withFileTypes: true });
177
+ } catch (error) {
178
+ if (error?.code !== "ENOENT") {
179
+ debugLog("config-lock.tmp_sweep_failed", { lockFile, code: error?.code, message: error?.message });
180
+ }
181
+ return;
182
+ }
183
+ for (const entry of entries) {
184
+ if (!entry.isFile()) continue;
185
+ const name = entry.name;
186
+ // ownTmp 显式跳过:调用方可能注入「一律判死」的 isPidAlive 测试桩,靠探活区分
187
+ // 不了自己的 tmp;误删自己的 tmp 会让 fs.link 失败于 ENOENT。
188
+ if (!name.startsWith(prefix) || name === ownTmp) continue;
189
+ const sibling = path.join(dir, name);
190
+ const owner = await readOwner(sibling);
191
+ if (!owner) continue; // body 读不出:无法证明无主,不动
192
+ if (owner.host !== os.hostname()) continue; // 异机无法探活,不动
193
+ // process.kill 接受正的 int32 PID;范围外的元数据不能作为死亡证明。
194
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0 || owner.pid > 0x7fff_ffff) continue;
195
+ try {
196
+ if (isPidAlive(owner.pid)) continue; // 存活(含僵尸/PID 复用)一律不删
197
+ } catch (error) {
198
+ debugLog("config-lock.tmp_sweep_failed", { tmpFile: sibling, code: error?.code, message: error?.message });
199
+ continue;
200
+ }
201
+ try {
202
+ await fs.rm(sibling, { force: true });
203
+ } catch (error) {
204
+ debugLog("config-lock.tmp_remove_failed", { tmpFile: sibling, code: error?.code, message: error?.message });
205
+ continue;
206
+ }
207
+ debugLog("config-lock.tmp_swept", { tmpFile: sibling, pid: owner.pid });
208
+ }
209
+ }
210
+
160
211
  /**
161
212
  * 获取 `<targetPath>.lock` 跨进程锁。返回一个幂等的、ownership-checked 的 `release()`。
162
213
  *
@@ -196,6 +247,10 @@ export async function acquireConfigLock(targetPath, {
196
247
  }
197
248
 
198
249
  try {
250
+ // 清扫处于自己的 tmp 清理保护区内,且失败不阻断锁获取。
251
+ await sweepAbandonedTmpFiles(lockFile, { isPidAlive, ownTmp: path.basename(tmpFile) }).catch((error) => {
252
+ debugLog("config-lock.tmp_sweep_failed", { lockFile, code: error?.code, message: error?.message });
253
+ });
199
254
  for (;;) {
200
255
  // 原子上位:lockFile 要么指向这个已写满的 inode,要么 EEXIST。link 之后
201
256
  // lockFile 与 tmpFile 是同一 inode 的两个名字,删 tmp 不影响 lockFile。
@@ -255,7 +310,11 @@ export async function acquireConfigLock(targetPath, {
255
310
  } finally {
256
311
  // tmp 在所有出口(成功 return / 超时 throw / 其它 error)都不再需要:成功时
257
312
  // lockFile 已是独立的第二个名字,删 tmp 只是丢一个多余的 link,inode 由 lockFile 存续。
258
- await fs.rm(tmpFile, { force: true }).catch(() => {});
313
+ // 删失败不再静默吞掉(残留 tmp 正是 Bug 7384601509 的累积源之一),与
314
+ // config-lock.hardlink_unsupported 同风格留痕,便于事后定位。
315
+ await fs.rm(tmpFile, { force: true }).catch((error) => {
316
+ debugLog("config-lock.tmp_remove_failed", { tmpFile, code: error?.code, message: error?.message });
317
+ });
259
318
  }
260
319
  }
261
320