From ca691a5be012f4e9d04910450e77c34b2a2d977f Mon Sep 17 00:00:00 2001 From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:48:04 +0300 Subject: [PATCH 1/4] test(cron): pin Task Scheduler XML encoding --- .../loop-js/src/cli/cron/schtasks.encoding.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/loop-js/src/cli/cron/schtasks.encoding.test.ts diff --git a/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts new file mode 100644 index 0000000..7341857 --- /dev/null +++ b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from "bun:test" +import { buildTaskXml, wrapperCommand } from "./schtasks.ts" + +test("Task Scheduler XML declares UTF-16 to match the file bytes", () => { + const xml = buildTaskXml({ + expr: "0 8 * * *", + dir: "C:\\proj", + command: wrapperCommand("C:\\proj\\.loop\\cron\\abc123.cmd"), + until: { settled: false }, + }) + + expect(xml.startsWith('')).toBe(true) +}) From 52fb051eb88e71596bd8529758f9d33533b8dde2 Mon Sep 17 00:00:00 2001 From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:49:47 +0300 Subject: [PATCH 2/4] fix(cron): write schtasks XML as UTF-16LE --- packages/loop-js/src/cli/cron/schtasks.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/loop-js/src/cli/cron/schtasks.ts b/packages/loop-js/src/cli/cron/schtasks.ts index a00b0e9..66174aa 100644 --- a/packages/loop-js/src/cli/cron/schtasks.ts +++ b/packages/loop-js/src/cli/cron/schtasks.ts @@ -116,7 +116,7 @@ export function wrapperCommand(wrapper: string): TaskCommand { export function buildTaskXml(opts: { expr: string; dir: string; command: TaskCommand; until: Until }): string { const trigger = triggersXml(expr.schtasks.schedule(opts.expr)) // a refused expr throws before anything is installed const desc = `${DESC}${opts.expr} ${formatUntil(opts.until)}` - return ` + return ` ${xmlEscape(desc)} ${trigger} @@ -176,7 +176,9 @@ export function systemSchtasks(): Schtasks { const dir = mkdtempSync(join(tmpdir(), "loop-cron-")) const file = join(dir, "task.xml") try { - writeFileSync(file, xml, "utf8") + // Match Task Scheduler's native XML representation: UTF-16LE with an explicit BOM. + // Without the BOM, `schtasks /Create /XML` cannot reliably determine the byte order. + writeFileSync(file, `\uFEFF${xml}`, "utf16le") const r = run(["/Create", "/F", "/TN", taskPath, "/XML", file]) if (r.status !== 0) throw bin.failure("schtasks create", r) } finally { From d6d25819cb6bb19e619aa3d997f2995d511e51f7 Mon Sep 17 00:00:00 2001 From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:10:28 +0300 Subject: [PATCH 3/4] test(cron): assert schtasks XML bytes --- packages/loop-js/src/cli/cron/schtasks.encoding.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts index 7341857..2534d83 100644 --- a/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts +++ b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts @@ -1,13 +1,16 @@ import { expect, test } from "bun:test" -import { buildTaskXml, wrapperCommand } from "./schtasks.ts" +import { buildTaskXml, taskXmlBytes, wrapperCommand } from "./schtasks.ts" -test("Task Scheduler XML declares UTF-16 to match the file bytes", () => { +test("Task Scheduler XML declaration and bytes agree on UTF-16", () => { const xml = buildTaskXml({ expr: "0 8 * * *", dir: "C:\\proj", command: wrapperCommand("C:\\proj\\.loop\\cron\\abc123.cmd"), until: { settled: false }, }) + const bytes = taskXmlBytes(xml) expect(xml.startsWith('')).toBe(true) + expect([...bytes.subarray(0, 2)]).toEqual([0xff, 0xfe]) + expect(bytes.toString("utf16le").startsWith('\uFEFF')).toBe(true) }) From 72466c28ba8c05a454b8d07ea5711c03bb1ec16e Mon Sep 17 00:00:00 2001 From: Mamdouh Aboammar <58908124+imMamdouhaboammar@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:11:20 +0300 Subject: [PATCH 4/4] fix(cron): centralize schtasks XML bytes --- packages/loop-js/src/cli/cron/schtasks.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/loop-js/src/cli/cron/schtasks.ts b/packages/loop-js/src/cli/cron/schtasks.ts index 66174aa..5562a5b 100644 --- a/packages/loop-js/src/cli/cron/schtasks.ts +++ b/packages/loop-js/src/cli/cron/schtasks.ts @@ -126,6 +126,11 @@ export function buildTaskXml(opts: { expr: string; dir: string; command: TaskCom ` } +/** The exact bytes `schtasks /Create /XML` receives: UTF-16LE plus an explicit byte-order mark. */ +export function taskXmlBytes(xml: string): Buffer { + return Buffer.from(`\uFEFF${xml}`, "utf16le") +} + /** Recover `{ expr, dir, until }` from a task XML we wrote; null if it is not one of ours. */ export function parseTaskXml(xml: string): Pick | null { const desc = firstMatch(xml, /([\s\S]*?)<\/Description>/) @@ -176,9 +181,7 @@ export function systemSchtasks(): Schtasks { const dir = mkdtempSync(join(tmpdir(), "loop-cron-")) const file = join(dir, "task.xml") try { - // Match Task Scheduler's native XML representation: UTF-16LE with an explicit BOM. - // Without the BOM, `schtasks /Create /XML` cannot reliably determine the byte order. - writeFileSync(file, `\uFEFF${xml}`, "utf16le") + writeFileSync(file, taskXmlBytes(xml)) const r = run(["/Create", "/F", "/TN", taskPath, "/XML", file]) if (r.status !== 0) throw bin.failure("schtasks create", r) } finally {