diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index d54e2f63c..da3b01fc0 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -360,6 +360,10 @@ ocx service status ocx service uninstall ``` +Windows では、`ocx service status` はタスク スケジューラの登録状態と、ID を確認済みの +OpenCodex プロキシへの到達性を別々に報告します。ローカライズされた `schtasks` の表は出力 +しないため、Windows のコード ページに関係なく概要を読めます。 + Windows でタスク スケジューラのエントリを作成するには昇格が必要です。認識できるローカライズ 済みのアクセス拒否テキストは、既存の案内経路をそのまま使用します。そのテキストが読めない場合、 コマンド形状が `/create /tn opencodex-proxy /xml <空でないパス> /f` と正確に一致し、終了 status diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index db267493e..1ff6fd825 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -379,6 +379,10 @@ ocx service status ocx service uninstall ``` +Windows에서 `ocx service status`는 작업 스케줄러 등록 상태와 신원이 확인된 OpenCodex 프록시의 +연결 상태를 따로 보고합니다. 로컬화된 `schtasks` 표를 출력하지 않으므로 Windows 코드 페이지와 +관계없이 요약을 읽을 수 있습니다. + Windows에서 작업 스케줄러 항목을 만들려면 권한 상승이 필요합니다. 인식 가능한 현지화 권한 거부 문자열은 기존 안내 경로를 그대로 사용합니다. 문자열을 읽을 수 없을 때는 명령 모양이 `/create /tn opencodex-proxy /xml <비어 있지 않은 경로> /f`와 정확히 일치하고, 종료 상태가 1이며, diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 1d37ba14b..568270ed3 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -439,6 +439,10 @@ ocx service status ocx service uninstall ``` +On Windows, `ocx service status` reports Task Scheduler registration separately from +identity-verified OpenCodex proxy reachability. It does not print the localized `schtasks` table, +so the summary remains readable across Windows code pages. + On Windows, creating the Task Scheduler entry requires elevation. Recognized localized access-denied text keeps the existing guidance path. If that text is unreadable, the fallback requires the owned command shape `/create /tn opencodex-proxy /xml /f`, status 1, diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index 0c1456354..0f19f7ead 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -401,6 +401,10 @@ ocx service status ocx service uninstall ``` +В Windows `ocx service status` отдельно сообщает о регистрации задания Планировщика и о доступности +прокси OpenCodex, подтверждённой проверкой его идентичности. Локализованная таблица `schtasks` не +выводится, поэтому сводка остаётся читаемой при любой кодовой странице Windows. + В Windows для создания задания Планировщика требуется повышение прав. Распознанный локализованный текст об отказе в доступе продолжает использовать прежний путь подсказки. Если текст нечитаем, независимый от языка fallback требует точную форму команды diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index 4895752d3..00f58c054 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -363,6 +363,9 @@ ocx service status ocx service uninstall ``` +在 Windows 上,`ocx service status` 会分别报告任务计划程序注册状态和经过身份验证的 OpenCodex +代理可达性。它不会输出本地化的 `schtasks` 表,因此摘要不受 Windows 代码页影响,始终可读。 + 在 Windows 上,创建任务计划程序条目需要提升权限。能识别的本地化“拒绝访问”文本继续使用现有 处理路径。如果该文本不可读,则回退路径要求命令形状严格为 `/create /tn opencodex-proxy /xml <非空路径> /f`、退出状态为 1,并确认当前 token 未提升;此时 diff --git a/src/service.ts b/src/service.ts index aecbd4886..703a4c690 100644 --- a/src/service.ts +++ b/src/service.ts @@ -17,6 +17,7 @@ import { isWslRuntime } from "./codex/home"; import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime"; import { isProcessAlive, stopProxy } from "./lib/process-control"; import { serviceApiTokenFilePath } from "./lib/service-secrets"; +import { findLiveProxy } from "./server/proxy-liveness"; import { randomUUID } from "node:crypto"; import { ELEVATION_REQUEST_TIMEOUT_MS, @@ -394,6 +395,70 @@ export type WindowsSchedulerTaskProbe = | { status: "absent" } | { status: "unknown"; detail: string }; +export type WindowsSchedulerProxyProbe = + | { status: "running"; port: number } + | { status: "not-running" } + | { status: "unknown" }; + +/** + * Render Task Scheduler status without exposing localized `schtasks` table output. + * The task probe answers installation state; the identity-checked health probe answers + * runtime state. Keep probe details out of this user-facing line because they can contain + * incorrectly decoded, locale-specific command output. + */ +export function formatWindowsSchedulerServiceStatus( + task: WindowsSchedulerTaskProbe, + proxy: WindowsSchedulerProxyProbe, +): string { + if (task.status === "present") { + if (proxy.status === "running") { + return `✅ service installed (Task Scheduler); OpenCodex proxy running on port ${proxy.port}.`; + } + if (proxy.status === "not-running") { + return "⚠️ service installed (Task Scheduler); OpenCodex proxy not running."; + } + return "⚠️ service installed (Task Scheduler); OpenCodex proxy status unknown."; + } + if (task.status === "absent") { + if (proxy.status === "running") { + return `❌ service not installed (Task Scheduler); OpenCodex proxy is running independently on port ${proxy.port}.`; + } + if (proxy.status === "unknown") { + return "❌ service not installed (Task Scheduler); OpenCodex proxy status unknown."; + } + return "❌ service not installed (Task Scheduler)."; + } + if (proxy.status === "running") { + return `⚠️ Task Scheduler registration unknown; OpenCodex proxy running on port ${proxy.port}.`; + } + if (proxy.status === "not-running") { + return "⚠️ service status unknown (Task Scheduler query failed); OpenCodex proxy not running."; + } + return "⚠️ service status unknown (Task Scheduler and proxy checks failed)."; +} + +export async function inspectWindowsSchedulerServiceStatus(io: { + probeTask?: () => WindowsSchedulerTaskProbe; + findProxy?: () => Promise<{ port: number } | null>; +} = {}): Promise { + let task: WindowsSchedulerTaskProbe; + try { + task = (io.probeTask ?? probeWindowsSchedulerTask)(); + } catch (error) { + task = { status: "unknown", detail: schtasksErrorDetail(error) }; + } + + let proxy: WindowsSchedulerProxyProbe; + try { + const live = await (io.findProxy ?? findLiveProxy)(); + proxy = live ? { status: "running", port: live.port } : { status: "not-running" }; + } catch { + proxy = { status: "unknown" }; + } + + return formatWindowsSchedulerServiceStatus(task, proxy); +} + function schtasksErrorDetail(error: unknown): string { return error instanceof Error ? error.message : String(error); } @@ -1756,8 +1821,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise { expect(windowsSchedulerTaskInstalled("opencodex-proxy")).toBe(true); }); + test("recognizes the task without exposing mojibake from localized table output", () => { + Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); + const localizedTable = [ + "����: \\", + "�۾� �̸� ���� ���� �ð� ����", + "======================================== ====================== ===============", + "opencodex-proxy N/A �غ�", + ].join("\n"); + setQuerySchtasksForTests(() => localizedTable); + + const result = formatWindowsSchedulerServiceStatus( + probeWindowsSchedulerTask("opencodex-proxy"), + { status: "running", port: 10100 }, + ); + + expect(result).toBe("✅ service installed (Task Scheduler); OpenCodex proxy running on port 10100."); + expect(result).not.toContain("����"); + expect(result).not.toContain("�۾�"); + }); + test("falls back to CSV listing when the specific query fails", () => { Object.defineProperty(process, "platform", { configurable: true, value: "win32" }); setQuerySchtasksForTests((args) => { @@ -83,6 +105,34 @@ describe("probeWindowsSchedulerTask", () => { }); }); +describe("formatWindowsSchedulerServiceStatus", () => { + test("reports task and identity-checked proxy state independently", () => { + expect(formatWindowsSchedulerServiceStatus({ status: "present" }, { status: "not-running" })) + .toBe("⚠️ service installed (Task Scheduler); OpenCodex proxy not running."); + expect(formatWindowsSchedulerServiceStatus({ status: "present" }, { status: "unknown" })) + .toBe("⚠️ service installed (Task Scheduler); OpenCodex proxy status unknown."); + expect(formatWindowsSchedulerServiceStatus({ status: "absent" }, { status: "running", port: 3593 })) + .toBe("❌ service not installed (Task Scheduler); OpenCodex proxy is running independently on port 3593."); + expect(formatWindowsSchedulerServiceStatus({ status: "absent" }, { status: "not-running" })) + .toBe("❌ service not installed (Task Scheduler)."); + expect(formatWindowsSchedulerServiceStatus({ status: "unknown", detail: "����" }, { status: "running", port: 10100 })) + .toBe("⚠️ Task Scheduler registration unknown; OpenCodex proxy running on port 10100."); + expect(formatWindowsSchedulerServiceStatus({ status: "unknown", detail: "����" }, { status: "not-running" })) + .toBe("⚠️ service status unknown (Task Scheduler query failed); OpenCodex proxy not running."); + }); + + test("keeps scheduler and runtime probe failures locale-independent", async () => { + const status = await inspectWindowsSchedulerServiceStatus({ + probeTask: () => { throw new Error("���� ����"); }, + findProxy: async () => { throw new Error("connection failure"); }, + }); + + expect(status).toBe("⚠️ service status unknown (Task Scheduler and proxy checks failed)."); + expect(status).not.toContain("����"); + expect(status).not.toContain("connection failure"); + }); +}); + describe("evaluateWindowsSchedulerInstallVerification", () => { const wscript = "C:\\Windows\\System32\\wscript.exe"; const launcher = "C:\\Users\\Test\\.opencodex\\opencodex-service-launcher.vbs";