Skip to content

[Bug]: decodeSchtasksOutput UTF-8 fallback mangles GBK schtasks output on zh-CN Windows — service repair/install fails with CJK account name #4691

Closed

Description

@mustardwheat

Summary

On a zh-CN Windows host (OEMCP/ACP 936), ocx service repair — and the Dashboard repair/install buttons — fail against a registration that OpenCodex itself created:

Service repair failed: Task Scheduler registration is not a recognized legacy OpenCodex definition; it was preserved for manual review.

Root cause: redirected schtasks /query /xml output follows the console output code page of the spawning process tree, not the XML document encoding. In any 936 console context (including the no-console background service on a zh-CN host), the bytes are GBK. decodeSchtasksOutput (src/service/windows-scheduler.ts, moved from src/service.ts in the 2.54 refactor) probes UTF-16 and then falls back to a plain UTF-8 decode, which turns the CJK account name inside <SessionStateChangeTrigger><UserId> into U+FFFD replacement characters. The correctly resolved expected identity [SID, MACHINE\user] then never matches the trigger scope, windowsTaskRegistrationHealthy() returns false, and repair aborts at the recognition gate. The same mojibake also rolls back fresh installs at the post-create verification ("...registration was created but failed the OpenCodex action/trigger or attempt-ownership verification. The invalid registration was rolled back.").

This is the remaining unwired call site of the decoder the project already built for exactly this class: decodeWindowsTextBytes (src/lib/windows-text.ts, UTF-16 → strict UTF-8 → locale legacy page incl. GBK) already fixed the sibling whoami/PowerShell decode in src/lib/windows-user-principal.ts (#2914, and #722 for CP949). Related: #3064 (non-ASCII profile path), #4106 (closed as not-planned; that reporter's console was CP 65001 — a different manifestation. The failure below reproduces deterministically under any 936 console).

Environment

  • OS: Windows 11, system locale zh-CN (ACP=936, OEMCP=936)
  • Local account name contains CJK characters
  • opencodex: observed on 2.47.0 → 2.55.0; dev branch fetched 2026-09-08 still has the same fallback

Reproduction

  1. ocx service install on a zh-CN host with a CJK account name. The task registers with SID-scoped session triggers; Task Scheduler export canonicalizes the SID back to the CJK account name.
  2. From a console with chcp 936 (or via the Dashboard, whose repair child runs without a console under the 936 service context), run ocx service repair.
  3. Observe the recognition error above (exit code 1; the subsequent serving check still prints the "repaired and serving" line because the proxy was never stopped).

Instrumented A/B against the real repairService (2.54.0, chcp 936):

  • statusWindowsXml() returns trigger scopes as MACHINE\???? (U+FFFD); resolveWindowsTaskDiagnosticUserId() returns the correct [SID, MACHINE\<CJK-name>]; windowsTaskRegistrationHealthy() → false → gate throws.
  • Same bytes through decodeWindowsTextBytes → correct account name; healthy → true; ocx service repair exits 0.

Suggested fix

One-line delegation plus import (running locally as a hotfix since 2.47.0; every npm i -g overwrite brings the bug back):

import { decodeWindowsTextBytes } from "../lib/windows-text";

export function decodeSchtasksOutput(buffer: Buffer): string {
  return decodeWindowsTextBytes(buffer);
}

Logs

❌ Service repair failed: Task Scheduler registration is not a recognized legacy OpenCodex definition; it was preserved for manual review.
✅ opencodex service repaired and serving on port 10100.   (serving check still passes; exit code is 1)

Activity

  1. added
    bugSomething isn't working
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Sep 15, 2026
  2. lidge-jun commented on Sep 15, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 74 / 80

    이 이슈는 zh-CN Windows(ACP/OEMCP 936)에서 ocx service repair / 대시보드 설치·수리가 자기 자신이 등록한 Task Scheduler 작업을 인식 불가 레거시로 잘못 판정하는 버그다. 지금 dev HEAD 270291170(패키지 2.56.0, #4683 tip)에도 리포터가 가리킨 호출 자리가 그대로 있다. 원인 경로는 짧다. schtasks /query /xml 리다이렉트 출력은 XML 문서 인코딩이 아니라, 띄운 프로세스 트리의 콘솔 코드 페이지를 따른다. 936 콘텍스트(대시보드 수리 자식, 콘솔 없는 백그라운드 서비스 포함)에서는 바이트가 GBK다. 그런데 src/service/windows-scheduler.ts의 decodeSchtasksOutput(약 20–41행)는 UTF-16 BOM/패턴만 보고, 아니면 plain UTF-8로 떨어진다. CJK 계정명이 들어 있는 <SessionStateChangeTrigger><UserId>가 U+FFFD로 깨지면, 올바르게 풀린 기대 identity [SID, MACHINE\\user]와 절대 안 맞는다. 그 결과 windowsTaskRegistrationHealthy()가 false가 되고, src/service/repair.ts 약 183–193행 recognition gate가 ‘Task Scheduler registration is not a recognized legacy OpenCodex definition…’를 던진다.

    프로젝트 안에는 이미 이 클래스용 디코더가 있다. src/lib/windows-text.ts의 decodeWindowsTextBytes(약 101–132행)는 UTF-16 → strict UTF-8 → 로케일 legacy(GBK 포함) 순이다. 같은 헬퍼가 windows-user-principal.ts(#2914, #722 CP949), tray, system-proxy, service-manager-probe.ts에서는 쓰이는데, 프로덕션 schtasks 디코드 자리만 아직 UTF-8 fallback이다. 리포터 A/B도 현재 구조와 맞다. 깨진 XML이면 healthy=false, 같은 바이트를 decodeWindowsTextBytes로 읽으면 healthy=true·repair exit 0.

    부가 효과가 크다. UserId가 깨진 채면 SID 스코프 업그레이드가 끝나지 않아 identityUpgradeNeeded(repair.ts 약 171–173행)가 매 수리마다 참이 될 수 있다. 그게 #4692 elevated ENAMETOOLONG 경로를 매번 타게 만드는 악화 조건이다, 리포터도 그렇게 연결해 두었다. #4106은 닫힌 not-planned이지만 그때 콘솔이 CP 65001이었다. 이번 재현은 936에서 결정적이라 별건으로 보는 게 맞다.

    라인 src/service/windows-scheduler.ts 20–41 (decodeSchtasksOutput) - UTF-16 다음이 plain UTF-8이라 GBK schtasks 출력이 CJK UserId를 U+FFFD로 만든다.
    라인 src/service/repair.ts 157–193 - 깨진 XML 때문에 healthy=false → ‘recognized legacy…’로 보존·중단. 설치 직후 verify/rollback도 같은 디코더를 탄다.
    경로 src/lib/windows-text.ts decodeWindowsTextBytes - 이미 UTF-16→strict UTF-8→locale legacy(GBK)를 구현. schtasks 자리만 미연결.
    경로 tests/windows/windows-scheduler-install-verification.test.ts decodeSchtasksOutput - UTF-16LE·plain UTF-8만 고정. GBK/zh-CN fixture가 없다.
    경로 tests/windows/windows-text-decoding.test.ts - zh-CN GBK 케이스는 이미 있다. schtasks 래퍼 쪽에 한 줄 위임 회귀만 추가하면 된다.

    메인테이너의 판단이 필요한 지점

    • 리포터 제안대로 decodeSchtasksOutput을 decodeWindowsTextBytes에 한 줄 위임할지(권장).
    • 위임 시 locale을 프로세스 Intl에 맡길지, repair/probe처럼 명시 locale을 넘길지.
    • #4692와 한 PR로 묶을지, 디코드 수정만 먼저 랜딩해 identity churn을 줄일지.
    • #4106을 관련 이슈로만 링크할지, 재오픈할지(재현 조건이 다름).

    너의 추천
    한 줄 위임 PR을 바로 받는다. decodeSchtasksOutput이 decodeWindowsTextBytes(buffer)만 호출하게 바꾸고, GBK XML fixture(CJK UserId + expected SID)로 healthy=true 회귀를 windows-scheduler-install-verification에 추가한다. #4692는 별 PR로 두되, 이 수정이 먼저면 elevated 경로 진입 빈도가 줄어든다. 이슈는 bug/platform/service 유지. 2.56.0 promote(#4687)와는 묶지 말 것. 다만 2.56.0 직후 핫픽스 후보로 우선순위 높다.

    이 댓글은 grok-bot이 작성했습니다

  3. added a commit that references this issue on Sep 16, 2026
  4. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Landed via #4749 at 52ec0a5

  5. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    Owner

    Landed via #4749 at c7924b8.

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

    bugSomething isn't workinglanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)serviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions