Repository navigation
fix(service): enumerate the machine once per startup, not once per inspection #2927
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,36 @@ export const SERVICE_PROBE_TIMEOUT_MS = 2_000; | |
| */ | ||
| export const SERVICE_PROBE_LISTING_TIMEOUT_MS = 20_000; | ||
|
|
||
| /** | ||
| * Caller-owned reuse of the whole-machine `schtasks` enumeration (#2923). | ||
| * | ||
| * The listing is the locale-neutral fallback, and on a host with hundreds of tasks | ||
| * it costs seconds. `startServer` inspects ownership twice on purpose — once | ||
| * before the startup cache decision, once before native lifecycle preparation — so | ||
| * a localized host paid the full enumeration TWICE before `Bun.serve`, up to 40s | ||
| * at the ceiling. | ||
| * | ||
| * The scope is deliberately the CALLER'S, not this module's. A module-level cache | ||
| * would leak across unrelated inspections (and, in tests, across cases), and the | ||
| * lifetime that is actually correct here is "one startup" — something only the | ||
| * caller knows. Passing an explicit object makes the sharing visible at both call | ||
| * sites and impossible anywhere else. | ||
| * | ||
| * What is shared is only the listing. The targeted `/query /tn` still runs on | ||
| * every inspection, so the race the second check exists to close is unaffected: a | ||
| * task that appears between the two checks is seen by the targeted query, which is | ||
| * what decides `present`. The listing only ever answers "our task is not among | ||
| * this machine's tasks", and only after the targeted query was not decisive. | ||
| */ | ||
| export interface ServiceProbeStartupCache { | ||
| /** Task names the machine reported, lowercased; absent until a listing succeeds. */ | ||
| listedTaskNames?: ReadonlySet<string>; | ||
| } | ||
|
|
||
| export function createServiceProbeStartupCache(): ServiceProbeStartupCache { | ||
| return {}; | ||
| } | ||
|
|
||
| export type ServiceManagerBackend = "launchd" | "systemd" | "scheduler" | "winsw"; | ||
|
|
||
| export interface ServiceManagerClaim { | ||
|
|
@@ -139,6 +169,12 @@ export interface ProbeDeps { | |
| readonly winswStatus?: () => "started" | "stopped" | "nonexistent" | "unknown"; | ||
| /** Test seam for redirected Windows legacy-codepage output. */ | ||
| readonly windowsLocale?: string; | ||
| /** | ||
| * Opt-in reuse of the expensive task listing across the inspections of ONE | ||
| * startup. Omit it and every inspection enumerates independently, exactly as | ||
| * before. | ||
| */ | ||
| readonly startupCache?: ServiceProbeStartupCache; | ||
| } | ||
|
|
||
| const LABEL = "com.opencodex.proxy"; | ||
|
|
@@ -552,6 +588,16 @@ function windowsTaskListContains(body: string, taskName: string): boolean { | |
| }); | ||
| } | ||
|
|
||
| /** The same normalization {@link windowsTaskListContains} applies, kept for reuse. */ | ||
| function windowsTaskListNames(body: string): ReadonlySet<string> { | ||
| const names = new Set<string>(); | ||
| for (const line of body.split(/\r?\n/)) { | ||
| const field = csvFirstField(line).replace(/\//g, "\\").replace(/^\\+/, ""); | ||
| if (field.length > 0) names.add(field.toLowerCase()); | ||
| } | ||
| return names; | ||
| } | ||
|
|
||
| /** | ||
| * The English message: a fast path on an English host, and nothing more. | ||
| * | ||
|
|
@@ -579,7 +625,7 @@ const SCHTASKS_TASK_NOT_FOUND_EN = /cannot find the file specified/i; | |
| * fallback, and only a successful list without our task proves absence. | ||
| */ | ||
| function probeWindowsTaskRegistration( | ||
| deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "windowsLocale">, | ||
| deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "windowsLocale" | "startupCache">, | ||
| ): { | ||
| registered: "present" | "absent" | "unknown"; | ||
| registeredXml: string; | ||
|
|
@@ -606,16 +652,26 @@ function probeWindowsTaskRegistration( | |
| return { registered: "absent", registeredXml: "" }; | ||
| } | ||
|
|
||
| const shared = deps.startupCache?.listedTaskNames; | ||
| if (shared !== undefined) { | ||
| return shared.has(windowsTaskName().toLowerCase()) | ||
| ? { registered: "unknown", registeredXml: "" } | ||
| : { registered: "absent", registeredXml: "" }; | ||
| } | ||
|
Comment on lines
+655
to
+660
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: # Inspect the repository convention and the changed branch with its directly bound
# definitions and callers. Do not execute repository code.
printf '%s\n' '--- scoped conventions ---'
find /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- target outline ---'
ast-grep outline src/service-manager-probe.ts
printf '%s\n' '--- target lines ---'
sed -n '600,700p' src/service-manager-probe.ts
printf '%s\n' '--- relevant symbols ---'
rg -n -C 4 'probeWindowsTaskRegistration|listedTaskNames|ServiceProbeStartupCache|schtasks|registeredXml|opencodex-proxy' src/service-manager-probe.ts srcRepository: lidge-jun/opencodex Length of output: 50375 🏁 Script executed: # The target source documents the intended fail-closed contract, but the caller
# and regression coverage determine whether the stale cache is reachable. Read
# only those directly bound sections and the scoped src convention.
printf '%s\n' '--- src convention ---'
cat /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/conventions/src.md
printf '%s\n' '--- cache contract and Windows caller ---'
sed -n '40,80p' src/service-manager-probe.ts
sed -n '700,825p' src/service-manager-probe.ts
printf '%s\n' '--- startup-cache call sites ---'
rg -n -C 8 'createServiceProbeStartupCache|startupCache|listedTaskNames' --glob '*.ts' --glob '*.tsx' src tests
printf '%s\n' '--- relevant tests ---'
rg -n -C 12 'startupCache|ambiguous|access denied|Access is denied|TASK_NOT_FOUND|schtasks|opencodex-proxy' tests/codex-service-manager-probe.test.tsRepository: lidge-jun/opencodex Length of output: 50375 Do not derive At 🤖 Prompt for AI Agents |
||
|
|
||
| const listed = deps.runRaw( | ||
| schtasks, | ||
| ["/query", "/fo", "CSV", "/nh"], | ||
| { timeoutMs: SERVICE_PROBE_LISTING_TIMEOUT_MS }, | ||
| ); | ||
| if (listed.spawnFailed || listed.timedOut || listed.status !== 0) { | ||
| // Deliberately not shared: caching a stall would turn one transient failure | ||
| // into an unprovable ownership for the rest of the startup, which refuses writes. | ||
| return { registered: "unknown", registeredXml: "" }; | ||
| } | ||
| const listing = decodeWindowsTextBytes(listed.stdout, { locale: deps.windowsLocale }) | ||
| || decodeWindowsTextBytes(listed.stderr, { locale: deps.windowsLocale }); | ||
| if (deps.startupCache) deps.startupCache.listedTaskNames = windowsTaskListNames(listing); | ||
| return windowsTaskListContains(listing, windowsTaskName()) | ||
| ? { registered: "unknown", registeredXml: "" } | ||
| : { registered: "absent", registeredXml: "" }; | ||
|
|
@@ -654,7 +710,7 @@ function probeWinswRegistration( | |
|
|
||
| function inspectWindows( | ||
| deps: Required<Pick<ProbeDeps, "runRaw" | "home">> | ||
| & Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">, | ||
| & Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale" | "startupCache">, | ||
| ): ServiceManagerInstallation { | ||
| const configDir = windowsConfigDirPath(deps); | ||
| const taskXmlPath = join(configDir, "opencodex-service-task.xml"); | ||
|
|
@@ -934,6 +990,7 @@ export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): Service | |
| configDir: deps.configDir, | ||
| winswStatus: deps.winswStatus, | ||
| windowsLocale: deps.windowsLocale, | ||
| startupCache: deps.startupCache, | ||
| }); | ||
| } | ||
| return unknown(`no service manager probe for platform ${platform}`); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the first inspection caches a listing without
opencodex-proxy, a task can be registered before the second inspection while leaving no local staged XML; if that inspection's targeted/xmlquery then returns a non-authoritative error such as localized access-denied or another transient nonzero response, this branch converts the stale cached miss directly intoabsent. A fresh listing could now contain the task and would have returnedunknown, so the new behavior can make ownership appearownedand admit unattended writes despite another service registration. Revalidate the listing or fail closed when the live targeted query is undecidable rather than allowing a cached negative to prove current absence.Useful? React with 👍 / 👎.