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.
Unified · Split
| @@ -2832,6 +2832,16 @@ const installService = (port: number, commandName: string, boot = false) => · 2831 unmodified lines | ||
| 2832 | 2832 | } |
| 2833 | 2833 | |
| 2834 | 2834 | 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 | + | |
| 2835 | 2845 | if (!backend.automated) { |
| 2836 | 2846 | // Unsupported platforms surface their manual steps via the install error. |
| 2837 | 2847 | yield* backend.install({ |
| @@ -9,6 +9,7 @@ import { · 8 unmodified lines | ||
| 9 | 9 | generateWindowsTaskXml, |
| 10 | 10 | getServiceBackend, |
| 11 | 11 | parseNetstatListenerPids, |
| 12 | + parseSchtasksRunning, | |
| 12 | 13 | } from "./service"; |
| 13 | 14 | |
| 14 | 15 | describe("service unit generation", () => { |
| @@ -188,6 +189,45 @@ describe("service unit generation", () => { · 173 unmodified lines | ||
| 188 | 189 | expect(parseNetstatListenerPids(netstat, 445)).toEqual([4]); |
| 189 | 190 | expect(parseNetstatListenerPids(netstat, 1234)).toEqual([]); |
| 190 | 191 | }); |
| 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 | + }); | |
| 191 | 231 | }); |
| 192 | 232 | |
| 193 | 233 | describe("service backend dispatch", () => { |
| @@ -680,18 +680,42 @@ const writeUtf16File = ( · 679 unmodified lines | ||
| 680 | 680 | }); |
| 681 | 681 | |
| 682 | 682 | /** |
| 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 | |
| 684 | 703 | * 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. | |
| 686 | 708 | */ |
| 709 | +const NETSTAT_LISTENER_REMOTES = new Set(["0.0.0.0:0", "[::]:0", "*:*"]); | |
| 710 | + | |
| 687 | 711 | export const parseNetstatListenerPids = (output: string, port: number): ReadonlyArray<number> => { |
| 688 | 712 | const pids = new Set<number>(); |
| 689 | 713 | for (const line of output.split(/\r?\n/)) { |
| 690 | 714 | const cols = line.trim().split(/\s+/); |
| 691 | − // TCP <local> <remote> LISTENING <pid> | |
| 715 | + // TCP <local> <remote(=wildcard when listening)> <state> <pid> | |
| 692 | 716 | if (cols.length < 5 || cols[0].toUpperCase() !== "TCP") continue; |
| 693 | − if (cols[3].toUpperCase() !== "LISTENING") continue; | |
| 694 | 717 | if (!cols[1].endsWith(`:${port}`)) continue; |
| 718 | + if (!NETSTAT_LISTENER_REMOTES.has(cols[2])) continue; | |
| 695 | 719 | const pid = Number.parseInt(cols[4], 10); |
| 696 | 720 | if (Number.isInteger(pid) && pid > 0) pids.add(pid); |
| 697 | 721 | } |
| @@ -844,15 +868,16 @@ const makeWindowsBackend = (): ServiceBackend => { · 146 unmodified lines | ||
| 844 | 868 | ], |
| 845 | 869 | }; |
| 846 | 870 | } |
| 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); | |
| 850 | 875 | return { |
| 851 | 876 | platform: "win32" as const, |
| 852 | 877 | registered: true, |
| 853 | 878 | running, |
| 854 | 879 | pid: null, |
| 855 | − detail: running ? [] : [`Scheduled task registered; current state: ${state}.`], | |
| 880 | + detail: running ? [] : ["Scheduled task registered but not currently running."], | |
| 856 | 881 | }; |
| 857 | 882 | }), |
| 858 | 883 | restart: () => |