Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

100 lines
4.0 KiB
JavaScript

#!/usr/bin/env bun
/**
* kill-stale-dev — kill leftover dev binaries running out of this repo's
* `target/` directory.
*
* Why: agent sessions spawn CLI trees (e.g. `bunx → codex-acp → MCP stdio
* bridges`), and the stdio bridges are the desktop binary itself
* (`nomifun-desktop.exe mcp-*-stdio`). If the dev app dies without cleanup
* (tauri dev rebuild, Ctrl+C, crash), the orphaned tree survives — and on
* Windows a running image locks its exe, so the next `cargo build` fails
* with `failed to remove file ... os error 5`. The in-process fix is the
* Job Object in nomifun-runtime (src/job.rs); this script is the preflight
* safety net that also clears stale processes left by builds that predate
* the fix, or by any path the job cannot cover.
*
* Cross-platform; never fails the dev command (always exits 0).
* Usage: bun scripts/kill-stale-dev.mjs [binary-name ...] (default: nomifun-desktop)
*/
import { execSync } from 'node:child_process';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const isWin = process.platform === 'win32';
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
// Binary names come from package.json — keep them shell/WQL/regex-inert.
const names = process.argv.slice(2).filter((n) => /^[\w.-]+$/.test(n));
if (names.length === 0) names.push('nomifun-desktop');
/** Escape a literal string for use inside an extended regex (pgrep -f). */
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/** [{pid, path}] of live processes whose executable lives under `<repo>/target/`. */
function staleProcesses(name) {
try {
if (isWin) {
const ps = `Get-CimInstance Win32_Process -Filter "Name='${name}.exe'" | Select-Object ProcessId,ExecutablePath | ConvertTo-Json -Compress`;
const out = execSync(`powershell -NoProfile -Command "${ps.replace(/"/g, '\\"')}"`, {
encoding: 'utf8',
}).trim();
if (!out) return [];
const rows = JSON.parse(out);
const list = Array.isArray(rows) ? rows : [rows];
const prefix = `${repoRoot.toLowerCase()}\\target\\`;
return list
.filter((r) => (r.ExecutablePath || '').toLowerCase().startsWith(prefix))
.map((r) => ({ pid: String(r.ProcessId), path: r.ExecutablePath }));
}
// macOS / Linux: pgrep -f matches the WHOLE command line, so anchor to
// argv[0] — otherwise a debugger/editor/tail whose arguments merely
// mention the path would be SIGKILLed too.
// pgrep exits non-zero when nothing matches → caught below.
const pattern = `^${escapeRegex(repoRoot)}/target/.*${escapeRegex(name)}`;
const out = execSync(`pgrep -f "${pattern}"`, { encoding: 'utf8' });
return out
.split(/\r?\n/)
.map((s) => s.trim())
.filter((p) => p && p !== String(process.pid))
.map((pid) => ({ pid, path: pattern }));
} catch {
return [];
}
}
function kill(pid) {
try {
// /T also takes the process tree — bridges hang off third-party CLIs.
if (isWin) execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
else execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
return true;
} catch (e) {
// taskkill 128 = "not found": an earlier /T tree-kill in this loop
// already took this pid down with its ancestor. That's success.
return isWin && e.status === 128;
}
}
let killedAny = false;
for (const name of names) {
const procs = staleProcesses(name);
if (procs.length === 0) {
console.log(`[kill-stale-dev] ${name}: no stale processes`);
continue;
}
for (const { pid, path } of procs) {
const ok = kill(pid);
killedAny = true;
console.log(`[kill-stale-dev] ${name}: ${ok ? 'killed' : 'FAILED to kill'} PID ${pid} (${path})`);
}
}
// Give the OS a beat to release file locks before cargo tries to relink.
// Best-effort like everything here — this script must never break the chain.
if (killedAny && isWin) {
try {
execSync('powershell -NoProfile -Command "Start-Sleep -Milliseconds 400"', { stdio: 'ignore' });
} catch {
/* ignore */
}
}