mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
refactor(daemon): extract schtasks cmd-set codec helpers
This commit is contained in:
56
src/daemon/cmd-set.ts
Normal file
56
src/daemon/cmd-set.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export type CmdSetAssignment = { key: string; value: string };
|
||||
|
||||
function escapeCmdSetAssignmentComponent(value: string): string {
|
||||
return value.replace(/\^/g, "^^").replace(/%/g, "%%").replace(/!/g, "^!").replace(/"/g, '^"');
|
||||
}
|
||||
|
||||
function unescapeCmdSetAssignmentComponent(value: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const ch = value[i];
|
||||
const next = value[i + 1];
|
||||
if (ch === "^" && (next === "^" || next === '"' || next === "!")) {
|
||||
out += next;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "%" && next === "%") {
|
||||
out += "%";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseCmdSetAssignment(line: string): CmdSetAssignment | null {
|
||||
const raw = line.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const quoted = raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2;
|
||||
const assignment = quoted ? raw.slice(1, -1) : raw;
|
||||
const index = assignment.indexOf("=");
|
||||
if (index <= 0) {
|
||||
return null;
|
||||
}
|
||||
const key = assignment.slice(0, index).trim();
|
||||
const value = assignment.slice(index + 1).trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
if (!quoted) {
|
||||
return { key, value };
|
||||
}
|
||||
return {
|
||||
key: unescapeCmdSetAssignmentComponent(key),
|
||||
value: unescapeCmdSetAssignmentComponent(value),
|
||||
};
|
||||
}
|
||||
|
||||
export function renderCmdSetAssignment(key: string, value: string): string {
|
||||
const escapedKey = escapeCmdSetAssignmentComponent(key);
|
||||
const escapedValue = escapeCmdSetAssignmentComponent(value);
|
||||
return `set "${escapedKey}=${escapedValue}"`;
|
||||
}
|
||||
66
src/daemon/schtasks.install.test.ts
Normal file
66
src/daemon/schtasks.install.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { installScheduledTask, readScheduledTaskCommand } from "./schtasks.js";
|
||||
|
||||
const schtasksCalls: string[][] = [];
|
||||
|
||||
vi.mock("./schtasks-exec.js", () => ({
|
||||
execSchtasks: async (argv: string[]) => {
|
||||
schtasksCalls.push(argv);
|
||||
return { code: 0, stdout: "", stderr: "" };
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
schtasksCalls.length = 0;
|
||||
});
|
||||
|
||||
describe("installScheduledTask", () => {
|
||||
it("writes quoted set assignments and escapes metacharacters", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-install-"));
|
||||
try {
|
||||
const env = {
|
||||
USERPROFILE: tmpDir,
|
||||
OPENCLAW_PROFILE: "default",
|
||||
};
|
||||
const { scriptPath } = await installScheduledTask({
|
||||
env,
|
||||
stdout: new PassThrough(),
|
||||
programArguments: ["node", "gateway.js", "--verbose"],
|
||||
environment: {
|
||||
OC_INJECT: "safe & whoami | calc",
|
||||
OC_CARET: "a^b",
|
||||
OC_PERCENT: "%TEMP%",
|
||||
OC_BANG: "!token!",
|
||||
OC_QUOTE: 'he said "hi"',
|
||||
},
|
||||
});
|
||||
|
||||
const script = await fs.readFile(scriptPath, "utf8");
|
||||
expect(script).toContain('set "OC_INJECT=safe & whoami | calc"');
|
||||
expect(script).toContain('set "OC_CARET=a^^b"');
|
||||
expect(script).toContain('set "OC_PERCENT=%%TEMP%%"');
|
||||
expect(script).toContain('set "OC_BANG=^!token^!"');
|
||||
expect(script).toContain('set "OC_QUOTE=he said ^"hi^""');
|
||||
expect(script).not.toContain("set OC_INJECT=");
|
||||
|
||||
const parsed = await readScheduledTaskCommand(env);
|
||||
expect(parsed?.environment).toMatchObject({
|
||||
OC_INJECT: "safe & whoami | calc",
|
||||
OC_CARET: "a^b",
|
||||
OC_PERCENT: "%TEMP%",
|
||||
OC_BANG: "!token!",
|
||||
OC_QUOTE: 'he said "hi"',
|
||||
});
|
||||
|
||||
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
||||
expect(schtasksCalls[1]?.[0]).toBe("/Create");
|
||||
expect(schtasksCalls[2]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,27 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
installScheduledTask,
|
||||
parseSchtasksQuery,
|
||||
readScheduledTaskCommand,
|
||||
resolveTaskScriptPath,
|
||||
} from "./schtasks.js";
|
||||
|
||||
const schtasksCalls: string[][] = [];
|
||||
|
||||
vi.mock("./schtasks-exec.js", () => ({
|
||||
execSchtasks: async (argv: string[]) => {
|
||||
schtasksCalls.push(argv);
|
||||
return { code: 0, stdout: "", stderr: "" };
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
schtasksCalls.length = 0;
|
||||
});
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseSchtasksQuery, readScheduledTaskCommand, resolveTaskScriptPath } from "./schtasks.js";
|
||||
|
||||
describe("schtasks runtime parsing", () => {
|
||||
it.each(["Ready", "Running"])("parses %s status", (status) => {
|
||||
@@ -246,50 +227,3 @@ describe("readScheduledTaskCommand", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("installScheduledTask", () => {
|
||||
it("writes quoted set assignments and escapes metacharacters", async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-schtasks-install-"));
|
||||
try {
|
||||
const env = {
|
||||
USERPROFILE: tmpDir,
|
||||
OPENCLAW_PROFILE: "default",
|
||||
};
|
||||
const { scriptPath } = await installScheduledTask({
|
||||
env,
|
||||
stdout: new PassThrough(),
|
||||
programArguments: ["node", "gateway.js", "--verbose"],
|
||||
environment: {
|
||||
OC_INJECT: "safe & whoami | calc",
|
||||
OC_CARET: "a^b",
|
||||
OC_PERCENT: "%TEMP%",
|
||||
OC_BANG: "!token!",
|
||||
OC_QUOTE: 'he said "hi"',
|
||||
},
|
||||
});
|
||||
|
||||
const script = await fs.readFile(scriptPath, "utf8");
|
||||
expect(script).toContain('set "OC_INJECT=safe & whoami | calc"');
|
||||
expect(script).toContain('set "OC_CARET=a^^b"');
|
||||
expect(script).toContain('set "OC_PERCENT=%%TEMP%%"');
|
||||
expect(script).toContain('set "OC_BANG=^!token^!"');
|
||||
expect(script).toContain('set "OC_QUOTE=he said ^"hi^""');
|
||||
expect(script).not.toContain("set OC_INJECT=");
|
||||
|
||||
const parsed = await readScheduledTaskCommand(env);
|
||||
expect(parsed?.environment).toMatchObject({
|
||||
OC_INJECT: "safe & whoami | calc",
|
||||
OC_CARET: "a^b",
|
||||
OC_PERCENT: "%TEMP%",
|
||||
OC_BANG: "!token!",
|
||||
OC_QUOTE: 'he said "hi"',
|
||||
});
|
||||
|
||||
expect(schtasksCalls[0]).toEqual(["/Query"]);
|
||||
expect(schtasksCalls[1]?.[0]).toBe("/Create");
|
||||
expect(schtasksCalls[2]).toEqual(["/Run", "/TN", "OpenClaw Gateway"]);
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
GatewayServiceRenderArgs,
|
||||
} from "./service-types.js";
|
||||
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
||||
import { parseCmdSetAssignment, renderCmdSetAssignment } from "./cmd-set.js";
|
||||
import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||
import { formatLine, writeFormattedLines } from "./output.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
@@ -42,61 +43,6 @@ function quoteCmdArg(value: string): string {
|
||||
return `"${value.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function escapeCmdSetAssignmentComponent(value: string): string {
|
||||
return value.replace(/\^/g, "^^").replace(/%/g, "%%").replace(/!/g, "^!").replace(/"/g, '^"');
|
||||
}
|
||||
|
||||
function unescapeCmdSetAssignmentComponent(value: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const ch = value[i];
|
||||
const next = value[i + 1];
|
||||
if (ch === "^" && (next === "^" || next === '"' || next === "!")) {
|
||||
out += next;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "%" && next === "%") {
|
||||
out += "%";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseCmdSetAssignment(line: string): { key: string; value: string } | null {
|
||||
const raw = line.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const quoted = raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2;
|
||||
const assignment = quoted ? raw.slice(1, -1) : raw;
|
||||
const index = assignment.indexOf("=");
|
||||
if (index <= 0) {
|
||||
return null;
|
||||
}
|
||||
const key = assignment.slice(0, index).trim();
|
||||
const value = assignment.slice(index + 1).trim();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
if (!quoted) {
|
||||
return { key, value };
|
||||
}
|
||||
return {
|
||||
key: unescapeCmdSetAssignmentComponent(key),
|
||||
value: unescapeCmdSetAssignmentComponent(value),
|
||||
};
|
||||
}
|
||||
|
||||
function renderCmdSetAssignment(key: string, value: string): string {
|
||||
const escapedKey = escapeCmdSetAssignmentComponent(key);
|
||||
const escapedValue = escapeCmdSetAssignmentComponent(value);
|
||||
return `set "${escapedKey}=${escapedValue}"`;
|
||||
}
|
||||
|
||||
function resolveTaskUser(env: GatewayServiceEnv): string | null {
|
||||
const username = env.USERNAME || env.USER || env.LOGNAME;
|
||||
if (!username) {
|
||||
@@ -132,20 +78,21 @@ export async function readScheduledTaskCommand(
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
const lower = line.toLowerCase();
|
||||
if (line.startsWith("@echo")) {
|
||||
continue;
|
||||
}
|
||||
if (line.toLowerCase().startsWith("rem ")) {
|
||||
if (lower.startsWith("rem ")) {
|
||||
continue;
|
||||
}
|
||||
if (line.toLowerCase().startsWith("set ")) {
|
||||
if (lower.startsWith("set ")) {
|
||||
const assignment = parseCmdSetAssignment(line.slice(4));
|
||||
if (assignment) {
|
||||
environment[assignment.key] = assignment.value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.toLowerCase().startsWith("cd /d ")) {
|
||||
if (lower.startsWith("cd /d ")) {
|
||||
workingDirectory = line.slice("cd /d ".length).trim().replace(/^"|"$/g, "");
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user