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..2534d83 --- /dev/null +++ b/packages/loop-js/src/cli/cron/schtasks.encoding.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test" +import { buildTaskXml, taskXmlBytes, wrapperCommand } from "./schtasks.ts" + +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) +}) diff --git a/packages/loop-js/src/cli/cron/schtasks.ts b/packages/loop-js/src/cli/cron/schtasks.ts index a00b0e9..5562a5b 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} @@ -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,7 +181,7 @@ export function systemSchtasks(): Schtasks { const dir = mkdtempSync(join(tmpdir(), "loop-cron-")) const file = join(dir, "task.xml") try { - writeFileSync(file, xml, "utf8") + writeFileSync(file, taskXmlBytes(xml)) const r = run(["/Create", "/F", "/TN", taskPath, "/XML", file]) if (r.status !== 0) throw bin.failure("schtasks create", r) } finally {