History

Windows service: locale-invariant status + guard --boot off-Windows

Address review on non-English Windows: - status() derived "running" from the schtasks `Status: Running` line, but both the label and the word are localized (fr: `État : En cours d'exécution`), so it always read as not-running on a non-English install. Detect via the locale-invariant SCHED_S_TASK_RUNNING result code (267009 / 0x41301) instead, extracted into the pure, tested parseSchtasksRunning(). - parseNetstatListenerPids matched the localized `LISTENING` state word. Identify a listener by its wildcard/zero remote endpoint (0.0.0.0:0 / [::]:0) instead; TCP and addresses are not localized. - `--boot` is documented Windows-only but was silently ignored on macOS/Linux. Warn when it is passed on a non-Windows platform. Tests add fr-FR / de-DE fixtures for both parsers. Verified on the Windows guest against a real running task's schtasks output: the old Status/Running regex regresses to false under fr-FR rendering while the 267009-based check stays correct.

Rhys Sullivan committed 4 months ago

Commit dac3e0a1 parent 8bd79753 files · +83 −8Browse files

Showing the changes to apps/cli (3 of 3 files). Show all

Unified · Split

3 files
  1. apps/cli/src/main.ts +10 −0
  2. apps/cli/src/service.test.ts +40 −0
  3. apps/cli/src/service.ts +33 −8
apps/cli/src/main.ts+10−0
@@ -2832,6 +2832,16 @@ const installService = (port: number, commandName: string, boot = false) => · 2831 unmodified lines
28322832 }
28332833
28342834 const backend = getServiceBackend();
2835+
2836+ // `--boot` only means something on Windows (a boot/S4U Scheduled Task). The
2837+ // launchd/systemd backends silently ignore the descriptor field, so warn
2838+ // rather than let a macOS/Linux caller believe it took effect.
2839+ if (boot && backend.platform !== "win32") {
2840+ console.warn(
2841+ `Note: --boot is a Windows-only option and has no effect on ${process.platform}; installing the standard login-based service.`,
2842+ );
2843+ }
2844+
28352845 if (!backend.automated) {
28362846 // Unsupported platforms surface their manual steps via the install error.
28372847 yield* backend.install({
apps/cli/src/service.test.ts+40−0
@@ -9,6 +9,7 @@ import { · 8 unmodified lines
99 generateWindowsTaskXml,
1010 getServiceBackend,
1111 parseNetstatListenerPids,
12+ parseSchtasksRunning,
1213 } from "./service";
1314
1415 describe("service unit generation", () => {
@@ -188,6 +189,45 @@ describe("service unit generation", () => { · 173 unmodified lines
188189 expect(parseNetstatListenerPids(netstat, 445)).toEqual([4]);
189190 expect(parseNetstatListenerPids(netstat, 1234)).toEqual([]);
190191 });
192+
193+ it("identifies listeners by wildcard remote, so it survives a localized state column", () => {
194+ // German netstat: the state word is "ABHÖREN"/"HERGESTELLT", but addresses and
195+ // "TCP" are not localized. Listener detection must not depend on the word.
196+ const deNetstat = [
197+ "Aktive Verbindungen",
198+ " Proto Lokale Adresse Remoteadresse Status PID",
199+ " TCP 127.0.0.1:4789 0.0.0.0:0 ABHÖREN 4072",
200+ " TCP 127.0.0.1:4789 127.0.0.1:51000 HERGESTELLT 8000",
201+ ].join("\r\n");
202+ expect(parseNetstatListenerPids(deNetstat, 4789)).toEqual([4072]);
203+ });
204+
205+ it("detects a running task via the locale-invariant result code, not the Status word", () => {
206+ // English verbose LIST output for a running task.
207+ const en = [
208+ "TaskName: \\ExecutorDaemon",
209+ "Status: Running",
210+ "Last Result: 267009",
211+ ].join("\r\n");
212+ expect(parseSchtasksRunning(en)).toBe(true);
213+
214+ // French Windows: both the label and the status word are translated, but the
215+ // SCHED_S_TASK_RUNNING code (267009 / 0x41301) is the same.
216+ const fr = [
217+ "Nom de tâche: \\ExecutorDaemon",
218+ "État: En cours d'exécution",
219+ "Dernier résultat: 267009",
220+ ].join("\r\n");
221+ expect(parseSchtasksRunning(fr)).toBe(true);
222+
223+ // A ready/terminated task (267014 = SCHED_S_TASK_TERMINATED) is not running.
224+ const ready = ["Status: Ready", "Last Result: 267014"].join(
225+ "\r\n",
226+ );
227+ expect(parseSchtasksRunning(ready)).toBe(false);
228+ // Hex form (some builds/locales) is also recognized.
229+ expect(parseSchtasksRunning("Last Result: 0x41301")).toBe(true);
230+ });
191231 });
192232
193233 describe("service backend dispatch", () => {
apps/cli/src/service.ts+33−8
@@ -680,18 +680,42 @@ const writeUtf16File = ( · 679 unmodified lines
680680 });
681681
682682 /**
683− * Parse `netstat -ano` output for the PIDs LISTENING on `port`. Pure, so it can
683+ * SCHED_S_TASK_RUNNING — the Task Scheduler HRESULT a task reports as its "Last
684+ * Result" while it is currently running (0x00041301 == 267009). schtasks prints
685+ * this in the verbose listing as a decimal; some Windows builds/locales print the
686+ * hex form. The numeric code is locale-invariant even though the surrounding
687+ * labels ("Last Result:", "Status:") and the human state word ("Running") are
688+ * translated on a non-English Windows.
689+ */
690+const SCHED_S_TASK_RUNNING = 267009;
691+
692+/**
693+ * Decide whether `schtasks /query /v /fo LIST` reports the task as currently
694+ * running, without depending on the localized "Status:" line. We look for the
695+ * locale-invariant SCHED_S_TASK_RUNNING result code instead. Pure + exported for
696+ * unit tests (including non-English fixtures).
697+ */
698+export const parseSchtasksRunning = (verboseListOutput: string): boolean =>
699+ new RegExp(`\\b(?:${SCHED_S_TASK_RUNNING}|0x0*41301)\\b`, "i").test(verboseListOutput);
700+
701+/**
702+ * Parse `netstat -ano` output for the PIDs listening on `port`. Pure, so it can
684703 * be unit-tested without a live socket. Matches both IPv4 (`127.0.0.1:PORT`) and
685− * IPv6 (`[::1]:PORT`) local endpoints in the LISTENING state.
704+ * IPv6 (`[::1]:PORT`) local endpoints. A listener is identified by its
705+ * wildcard/zero remote endpoint (`0.0.0.0:0` / `[::]:0`) rather than the state
706+ * column, because that column ("LISTENING") is localized on a non-English
707+ * Windows while the addresses and the `TCP` token are not.
686708 */
709+const NETSTAT_LISTENER_REMOTES = new Set(["0.0.0.0:0", "[::]:0", "*:*"]);
710+
687711 export const parseNetstatListenerPids = (output: string, port: number): ReadonlyArray<number> => {
688712 const pids = new Set<number>();
689713 for (const line of output.split(/\r?\n/)) {
690714 const cols = line.trim().split(/\s+/);
691− // TCP <local> <remote> LISTENING <pid>
715+ // TCP <local> <remote(=wildcard when listening)> <state> <pid>
692716 if (cols.length < 5 || cols[0].toUpperCase() !== "TCP") continue;
693− if (cols[3].toUpperCase() !== "LISTENING") continue;
694717 if (!cols[1].endsWith(`:${port}`)) continue;
718+ if (!NETSTAT_LISTENER_REMOTES.has(cols[2])) continue;
695719 const pid = Number.parseInt(cols[4], 10);
696720 if (Number.isInteger(pid) && pid > 0) pids.add(pid);
697721 }
@@ -844,15 +868,16 @@ const makeWindowsBackend = (): ServiceBackend => { · 146 unmodified lines
844868 ],
845869 };
846870 }
847− // schtasks LIST output has a localized "Status:" line: Running/Ready/Disabled.
848− const state = /^\s*Status:\s*(\w+)/m.exec(result.stdout)?.[1] ?? "Unknown";
849− const running = state === "Running";
871+ // Detect "running" via the locale-invariant SCHED_S_TASK_RUNNING result
872+ // code, not the translated "Status: Running" line (which is localized on
873+ // a non-English Windows and would otherwise always read as not-running).
874+ const running = parseSchtasksRunning(result.stdout);
850875 return {
851876 platform: "win32" as const,
852877 registered: true,
853878 running,
854879 pid: null,
855− detail: running ? [] : [`Scheduled task registered; current state: ${state}.`],
880+ detail: running ? [] : ["Scheduled task registered but not currently running."],
856881 };
857882 }),
858883 restart: () =>