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 {