diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 59e26108466..33aeaaf715c 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -355,6 +355,9 @@ Windows에서 Task Scheduler 항목을 만들려면 권한 상승이 필요합 시작할 때 `another process owns the runtime mutation lease`가 보고되거나 `ocx service status`에 `Runtime mutation lease busy`가 표시되면, 프록시가 실행 중이 아니더라도 이 리스가 시작이나 서비스 변경을 막고 있는 것입니다. 메시지에는 잠금 경로, 기록된 PID, 그 PID가 현재 살아 있는지 여부, 확인할 수 있는 경우 실행 파일 이름, 리스 경과 시간이 들어 있습니다. 프로세스의 신원은 검증되지 않습니다. PID가 재사용되었을 수 있으므로, 생존 여부와 실행 파일 이름은 지금 그 PID를 쓰고 있는 프로세스에 대한 정보입니다. 작업이 끝날 때까지 기다린 뒤 다시 시도하세요. 이 PID만 보고 잠금을 삭제하거나 프로세스를 중지하지 마세요. 리스 경과 시간이 30초를 넘고 기록된 PID가 더 이상 살아 있지 않으면 이후의 변경 시도가 오래된 리스를 회수할 수 있습니다. `ocx service status`는 리스를 확인만 합니다. + +Windows에서는 이 공용 Node/Bun 진단 경로가 실행 파일 이름 조회용 프로그램을 실행하지 않습니다. 기록된 PID, 생존 여부, 리스 경과 시간과 복구 안내는 계속 표시됩니다. + ### `ocx codex-shim ` macOS와 Linux에서 `ocx codex-shim install`은 확정된 OpenCodex 홈에 전용 wrapper `/bin/codex`와 셸에서 읽을 `/codex-shell-env.sh`를 설치합니다. 원래 런처는 brew, npm, fnm이 설치한 위치에 그대로 있어 패키지 관리자 업데이트와 버전 롤백 뒤에도 다시 감쌀 필요가 없습니다. Windows는 기존처럼 스크립트 런처를 제자리에서 감싸며 실제 `codex.exe`는 건드리지 않습니다. Windows에서 `codex.exe`만 제공된다면 `ocx service install`을 사용하세요. diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 7baa048e2ab..3ecca9868e1 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -827,6 +827,9 @@ that PID now. Wait for the operation to finish and retry; do not delete the lock process based only on this PID. A later mutation attempt can reclaim a stale lease once its age exceeds 30 seconds and the recorded PID is no longer alive; status only inspects it. + +On Windows, this shared Node/Bun diagnostic does not launch an executable to look up the image name; the recorded PID, liveness, lease age and recovery guidance remain available. + ### `ocx codex-shim ` On macOS and Linux, install a private autostart wrapper at `/bin/codex` and a diff --git a/src/service/ownership-mutation-lease.d.mts b/src/service/ownership-mutation-lease.d.mts index c01d63f7279..8ee827777ed 100644 --- a/src/service/ownership-mutation-lease.d.mts +++ b/src/service/ownership-mutation-lease.d.mts @@ -5,7 +5,7 @@ export interface OwnershipMutationLeaseOptions { readonly processAlive?: (pid: number) => boolean; readonly beforeRelease?: (lockPath: string) => void; readonly joinToken?: string; - /** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */ + /** Names the current executable at the recorded PID, identity unverified; uses `ps` on POSIX and omits the default image lookup on Windows. */ readonly processImage?: (pid: number) => string | null; } diff --git a/src/service/ownership-mutation-lease.mjs b/src/service/ownership-mutation-lease.mjs index 07eab51e1a9..9b95bd5aa7f 100644 --- a/src/service/ownership-mutation-lease.mjs +++ b/src/service/ownership-mutation-lease.mjs @@ -112,13 +112,11 @@ function readIncompleteOwner(path) { /** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */ function processImage(pid) { try { - if (process.platform === "win32") { - const listed = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { - encoding: "utf8", timeout: 1_000, windowsHide: true, - }); - const match = /^"([^"]+)","(\d+)"/.exec((listed.stdout ?? "").trim()); - return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null; - } + // This module also runs in the Node npm/pnpm launcher, before Bun is available. + // Without a kernel-backed system-directory resolver, omit the optional Windows + // image label rather than searching the caller's directory/PATH or trusting env roots. + // PID, liveness, age and lease ownership decisions do not depend on this label. + if (process.platform === "win32") return null; const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 }); const command = (listed.stdout ?? "").trim(); return listed.status === 0 && command ? basename(command) : null; diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 4d72608adbd..64cc27a2950 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -162,7 +162,7 @@ an owner claim committed during the early probe cannot be followed by shared Cod When that lease is still busy after its wait, `acquireOwnershipMutationLease` (`src/service/ownership-mutation-lease.mjs`) names the holder in its error and on the error's `holder` field. That means the owner's PID, whether it is alive, a live holder's executable name -when `tasklist`/`ps` answers within a second, and the owner's age on the clock stale recovery +when POSIX `ps` answers within a second (the shared Node/Bun Windows path omits image lookup), and the owner's age on the clock stale recovery uses, plus the 30-second reclaim rule. `ocx service status` prints the same holder line whenever the lease directory exists. That read never reclaims. The connected-client branch, which returns into `startClientRuntime` before the server path, diff --git a/structure/runtime.md b/structure/runtime.md index d3eac545039..ae428036666 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -593,7 +593,7 @@ manager children. A replacement refusal passes through owner-aware recovery: onl owner revives the stopped runtime; foreign ownership stays transferred and unknown ownership remains a reported recovery requirement. Dashboard restart delegates the lease token to its `service repair` child, and releases the lease before that refresh: the service manager spawns `ocx start` outside the worker's process tree with no token, so the managed child must take the lease itself (#5760). Before the direct-start fallthrough kills or spawns on the port, the worker takes the lease back — a refresh that stayed claimed stops the restart — and re-runs the recorded-owner veto under it because a claim could have landed during the unleased refresh window. Direct start holds the same lease through bind plus PID and runtime-address publication. If listener rollback cannot prove the socket closed, the process retains its lease until exit. The registration is never deleted; `ocx service install` releases the marker only after the -registration succeeds. Busy lease diagnostics name the recorded PID, current liveness and executable, and lease age. The process identity is unverified because a PID may be reused. +registration succeeds. Busy lease diagnostics name the recorded PID, current liveness, optional executable (omitted by default on Windows), and lease age. The process identity is unverified because a PID may be reused. Bun updater lease and recovery behavior follows the [update transaction contract](ops/service-and-sidecars.md#bun-updater-ownership-transaction). diff --git a/tests/service/service-ownership-state.test.ts b/tests/service/service-ownership-state.test.ts index 6b8be911d31..3db3174042c 100644 --- a/tests/service/service-ownership-state.test.ts +++ b/tests/service/service-ownership-state.test.ts @@ -7,7 +7,8 @@ * therefore lasted until the next repair — from a tray helper, from `ocx update`, from a * doctor suggestion — and nothing said it had gone. */ -import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"; +import * as childProcess from "node:child_process"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, utimesSync, writeFileSync } from "node:fs"; import { createTempHome, type TempHome } from "../helpers/temp-home"; import { repoPath } from "../helpers/repo-root"; @@ -684,6 +685,29 @@ describe("a busy runtime mutation lease names its holder (#6492)", () => { expect(line).toContain("stale leases are reclaimed after 30s once the recorded PID is no longer alive."); }); + test("Windows busy-lease diagnostics never search for an executable", () => { + hold(6236); + const paths = [serviceStatePath()]; + const descriptor = Object.getOwnPropertyDescriptor(process, "platform")!; + const spawned = spyOn(childProcess, "spawnSync").mockImplementation(() => ({ + status: 0, stdout: '"untrusted-tasklist.exe","6236"', stderr: "", pid: 1, + output: [null, '"untrusted-tasklist.exe","6236"', ""], signal: null, + })); + try { + Object.defineProperty(process, "platform", { ...descriptor, value: "win32" }); + const options = { now: () => 2_000, processAlive: () => true }; + expect(inspectOwnershipMutationLease(paths, options)).toMatchObject({ pid: 6236, alive: true, image: null }); + expect(ownershipMutationLeaseStatusLine(paths, options)).toContain("recorded PID 6236 [alive, identity unverified]"); + expect(() => acquireOwnershipMutationLease(paths, { ...options, waitMs: 0 })) + .toThrow("another process owns the runtime mutation lease"); + expect(spawned).not.toHaveBeenCalled(); + } finally { + Object.defineProperty(process, "platform", descriptor); + spawned.mockRestore(); + } + expect(existsSync(lockDir())).toBe(true); + }); + test("inspection only reads: a dead, stale holder is reported, not reclaimed", () => { hold(6236); const holder = inspectOwnershipMutationLease([serviceStatePath()], { now: () => 600_000, processAlive: () => false });