Skip to content

Commit 5dd670f

Browse files
committed
fix(update): reject stub Bun probes and re-kill reused reclaim PIDs
Gate live-package Bun bind probes with isRealBunBinary so a postinstall stub cannot block pinned start, and clear the reclaim killed-set when a LISTEN owner is dead so a respawn that reuses the PID is not skipped.
1 parent 8e732a8 commit 5dd670f

3 files changed

Lines changed: 41 additions & 5 deletions

File tree

src/server/port-reclaim.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
*
55
* Killing is never the default. A process may be killed only when the caller
66
* sets `killOcxHolders` and either supplies a non-empty `onlyKillPids` allowlist
7-
* or enables `killAllOcxOnPort` for revalidated ocx listeners. Foreign (non-ocx)
8-
* processes are never killed.
7+
* (trusted teardown PIDs, including allowlisted holders that fail ocx revalidate)
8+
* or enables `killAllOcxOnPort` for revalidated ocx listeners. Unknown foreign
9+
* (non-ocx, non-allowlisted) processes are never killed.
910
*/
1011
import { execFileSync } from "node:child_process";
1112
import { verifyPidIdentity } from "../config";
@@ -222,7 +223,12 @@ export async function reclaimListenPort(
222223

223224
for (const pid of scan.pids) {
224225
if (pid === process.pid) continue;
225-
if (!isAliveFn(pid)) continue; // Windows may still list a dead owner briefly
226+
if (!isAliveFn(pid)) {
227+
// Clear "already tried" so a later respawn that reuses this PID slot
228+
// is not skipped (Windows service :loop / npm rename respawns).
229+
killed.delete(pid);
230+
continue; // Windows may still list a dead owner briefly
231+
}
226232
const isOcx = verifyOcxFn(pid) === pid;
227233
const allowlisted = allowedKillPids.has(pid);
228234
// Pre-update PIDs can fail verify while still LISTENing (dead owner still

src/update/job.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
updateCommandStr,
3636
} from "./index";
3737
import { isNewer } from "./notify";
38+
import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
3839
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
3940

4041
const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
@@ -147,14 +148,15 @@ function spawnBindProbe(bin: string, script: string): boolean {
147148

148149
/**
149150
* Live global package Bun — not the npm rename tree the update worker may still
150-
* be executing from (`@bitkyc08/.opencodex-*`).
151+
* be executing from (`@bitkyc08/.opencodex-*`). Reject the tiny postinstall
152+
* stub so probes fall back to the worker runtime instead of failing forever.
151153
*/
152154
function livePackageBunPath(): string | null {
153155
const launcher = packageLauncherPath();
154156
const root = join(dirname(launcher), "..");
155157
for (const name of ["bun.exe", "bun"]) {
156158
const candidate = join(root, "node_modules", "bun", "bin", name);
157-
if (existsSync(candidate)) return candidate;
159+
if (isRealBunBinary(candidate)) return candidate;
158160
}
159161
return null;
160162
}

tests/port-reclaim.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,4 +556,32 @@ describe("reclaimListenPort", () => {
556556
expect(killed).toEqual([14772]);
557557
expect(dropped).toEqual([10100]);
558558
});
559+
560+
test("dead ghost then same PID reused is killed again under killAllOcxOnPort", async () => {
561+
const killed: number[] = [];
562+
let phase: "first-live" | "ghost" | "reuse" = "first-live";
563+
let available = false;
564+
await expect(reclaimListenPort(10100, "127.0.0.1", {
565+
timeoutMs: 200,
566+
intervalMs: 20,
567+
scanIntervalMs: 20,
568+
dropTcpRows: false,
569+
killOcxHolders: true,
570+
killAllOcxOnPort: true,
571+
onlyKillPids: [],
572+
isAvailableFn: async () => available,
573+
listListenPidsFn: () => [4242],
574+
isAliveFn: () => phase !== "ghost",
575+
verifyOcxFn: pid => pid,
576+
killFn: pid => {
577+
killed.push(pid);
578+
if (phase === "first-live") phase = "ghost";
579+
else if (phase === "reuse") available = true;
580+
},
581+
sleepMs: async () => {
582+
if (phase === "ghost") phase = "reuse";
583+
},
584+
})).resolves.toBe(true);
585+
expect(killed).toEqual([4242, 4242]);
586+
});
559587
});

0 commit comments

Comments
 (0)