Skip to content

Windows (non-English / Bun): ocx service & ocx update abort — probeScmRegistration fails to detect ERROR_SERVICE_DOES_NOT_EXIST (1060) #216

Closed

Description

@brunoflma

Summary

On non-English Windows (and generally under the bundled Bun runtime), ocx service aborts during install with a false "native service still exists" error, even when the opencodex-proxy-native SCM service does not exist. The same faulty detection path is reused by ocx update (its service-restart step), so updating a service-managed proxy is affected too.

Root cause: probeScmRegistration() in src/lib/winsw.ts cannot recognize sc.exe's ERROR_SERVICE_DOES_NOT_EXIST (1060) result, so statusWinswRaw() returns "unknown" instead of "nonexistent", and installWindows() refuses to proceed.

Environment

  • OS: Windows 11 (pt-BR locale), 10.0.26200
  • opencodex: 2.7.28 (reproduces the same code on 2.7.30)
  • Runtime: bundled Bun 1.3.14
  • Install: global via fnm-managed Node

Symptom

$ ocx service
error: Cannot remove the native service before switching to Task Scheduler:
Cannot verify the native service registration (sc.exe query failed).
Uninstall aborted; check 'sc query opencodex-proxy-native' and retry.
    at installWindows (.../src/service.ts:483)

…yet the service genuinely does not exist:

$ sc query opencodex-proxy-native
[SC] EnumQueryServicesStatus:OpenService FALHA 1060:
O serviço especificado não existe como serviço instalado.

There is no ~/.opencodex/winsw/ directory and no opencodex-proxy-native SCM registration.

Root cause

statusWinswRaw() → probeScmRegistration() (src/lib/winsw.ts) classifies the sc query failure. Only a confirmed 1060 is allowed to prove absence; anything else becomes "error" → statusWinswRaw() returns "unknown" → lifecycle callers assume a service may exist. The 1060 check has two portability gaps:

  1. Exit-code truncation under Bun. sc.exe exits with code 1060, but Bun on Windows truncates the process exit status to 8 bits, so the thrown error reports e.status === 36 (1060 % 256 = 36). The guard e.status === 1060 is therefore never true under Bun.
  2. English-only text match. The fallback /FAILED 1060/i does not match localized sc.exe output. On pt-BR the line is ... FALHA 1060: ..., so the regex misses it.

With both signals failing, probeScmRegistration() returns "error" (fail-closed), which is the intended behavior for a genuinely unreadable SCM — but here the SCM answered clearly, we just failed to parse a localized/truncated "does not exist".

Reproduction (Bun on Windows)

const { execFileSync } = require("node:child_process");
const { join } = require("node:path");
const sc = join(process.env.SystemRoot || "C\\Windows", "System32", "sc.exe");
try {
  execFileSync(sc, ["query", "opencodex-proxy-native"], { encoding: "utf8", stdio: ["ignore","pipe","pipe"] });
} catch (e) {
  const text = [e.stderr, e.stdout, e.message].map(v => typeof v === "string" ? v : "").join("\n");
  console.log("e.status =", e.status);              // -> 36  (NOT 1060, truncated by Bun)
  console.log("/FAILED 1060/i:", /FAILED 1060/i.test(text)); // -> false (pt-BR says "FALHA 1060")
  console.log("/\\b1060\\b/:", /\b1060\b/.test(text));       // -> true
}

Output on a pt-BR host:

e.status = 36
/FAILED 1060/i: false
/\b1060\b/: true

Proposed fix

Match the locale-independent numeric code anywhere in the captured sc.exe output. The string 1060 is present regardless of UI language, and this is immune to the Bun exit-code truncation. In src/lib/winsw.ts, probeScmRegistration():

-    if (e.status === 1060 || /FAILED 1060/i.test(text)) return false;
+    // 1060 = ERROR_SERVICE_DOES_NOT_EXIST. Two portability gaps make the original
+    // check fail:
+    //   * Bun on Windows truncates the exit code to 8 bits, so a 1060 exit arrives
+    //     as `e.status === 36` (1060 % 256) and the numeric guard never matches.
+    //   * sc.exe localizes its output (e.g. pt-BR "FALHA 1060"), so /FAILED 1060/i
+    //     never matches on non-English hosts.
+    // The numeric code itself is locale-independent and survives truncation, so
+    // match it anywhere in the captured stdout/stderr/message.
+    if (e.status === 1060 || /\b1060\b/.test(text)) return false;

This keeps the fail-closed behavior for genuinely unreadable results (access denied, sc.exe missing, etc.) while correctly proving absence when the SCM reports 1060 in any locale.

Impact / notes

  • Blocks ocx service install on non-English Windows even with an elevated shell (the abort happens in the SCM-detection step, before schtasks).
  • Also affects ocx update when a background service is installed: its restart step calls ocx service install, which hits the same abort.
  • After applying the one-line change locally, ocx service proceeds to schtasks /create and installs the Task Scheduler backend as expected.

Happy to open a PR with the change if preferred.

Activity

  1. lidge-jun commented on Jul 21, 2026

    @lidge-jun
    Owner

    v2.7.31에 수정 반영했습니다. locale에 의존하지 않는 Win32 1060 감지와 서비스 lifecycle fail-closed 처리를 적용했고, Windows 포함 CI를 통과했습니다. 반영 커밋: fbd96c1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions