mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 08:58:37 +00:00
refactor(daemon): extract windows cmd argv helpers
This commit is contained in:
42
src/daemon/cmd-argv.test.ts
Normal file
42
src/daemon/cmd-argv.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseCmdScriptCommandLine, quoteCmdScriptArg } from "./cmd-argv.js";
|
||||||
|
|
||||||
|
describe("cmd argv helpers", () => {
|
||||||
|
it.each([
|
||||||
|
"plain",
|
||||||
|
"with space",
|
||||||
|
"safe&whoami",
|
||||||
|
"safe|whoami",
|
||||||
|
"safe<in",
|
||||||
|
"safe>out",
|
||||||
|
"safe^caret",
|
||||||
|
"%TEMP%",
|
||||||
|
"!token!",
|
||||||
|
'he said "hi"',
|
||||||
|
])("round-trips single arg: %p", (arg) => {
|
||||||
|
const encoded = quoteCmdScriptArg(arg);
|
||||||
|
expect(parseCmdScriptCommandLine(encoded)).toEqual([arg]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trips mixed command lines", () => {
|
||||||
|
const args = [
|
||||||
|
"node",
|
||||||
|
"gateway.js",
|
||||||
|
"--display-name",
|
||||||
|
"safe&whoami",
|
||||||
|
"--percent",
|
||||||
|
"%TEMP%",
|
||||||
|
"--bang",
|
||||||
|
"!token!",
|
||||||
|
"--quoted",
|
||||||
|
'he said "hi"',
|
||||||
|
];
|
||||||
|
const encoded = args.map(quoteCmdScriptArg).join(" ");
|
||||||
|
expect(parseCmdScriptCommandLine(encoded)).toEqual(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects CR/LF in command arguments", () => {
|
||||||
|
expect(() => quoteCmdScriptArg("bad\narg")).toThrow(/Command argument cannot contain CR or LF/);
|
||||||
|
expect(() => quoteCmdScriptArg("bad\rarg")).toThrow(/Command argument cannot contain CR or LF/);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
src/daemon/cmd-argv.ts
Normal file
26
src/daemon/cmd-argv.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
||||||
|
import { assertNoCmdLineBreak } from "./cmd-set.js";
|
||||||
|
|
||||||
|
export function quoteCmdScriptArg(value: string): string {
|
||||||
|
assertNoCmdLineBreak(value, "Command argument");
|
||||||
|
if (!value) {
|
||||||
|
return '""';
|
||||||
|
}
|
||||||
|
const escaped = value.replace(/"/g, '\\"').replace(/%/g, "%%").replace(/!/g, "^!");
|
||||||
|
if (!/[ \t"&|<>^()%!]/g.test(value)) {
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
return `"${escaped}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unescapeCmdScriptArg(value: string): string {
|
||||||
|
return value.replace(/\^!/g, "!").replace(/%%/g, "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCmdScriptCommandLine(value: string): string[] {
|
||||||
|
// Script renderer escapes quotes (`\"`) and cmd expansions (`%%`, `^!`).
|
||||||
|
// Keep all other backslashes literal so Windows drive/UNC paths survive.
|
||||||
|
return splitArgsPreservingQuotes(value, { escapeMode: "backslash-quote-only" }).map(
|
||||||
|
unescapeCmdScriptArg,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
export type CmdSetAssignment = { key: string; value: string };
|
export type CmdSetAssignment = { key: string; value: string };
|
||||||
|
|
||||||
|
export function assertNoCmdLineBreak(value: string, field: string): void {
|
||||||
|
if (/[\r\n]/.test(value)) {
|
||||||
|
throw new Error(`${field} cannot contain CR or LF in Windows task scripts.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function escapeCmdSetAssignmentComponent(value: string): string {
|
function escapeCmdSetAssignmentComponent(value: string): string {
|
||||||
return value.replace(/\^/g, "^^").replace(/%/g, "%%").replace(/!/g, "^!").replace(/"/g, '^"');
|
return value.replace(/\^/g, "^^").replace(/%/g, "%%").replace(/!/g, "^!").replace(/"/g, '^"');
|
||||||
}
|
}
|
||||||
@@ -50,6 +56,8 @@ export function parseCmdSetAssignment(line: string): CmdSetAssignment | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderCmdSetAssignment(key: string, value: string): string {
|
export function renderCmdSetAssignment(key: string, value: string): string {
|
||||||
|
assertNoCmdLineBreak(key, "Environment variable name");
|
||||||
|
assertNoCmdLineBreak(value, "Environment variable value");
|
||||||
const escapedKey = escapeCmdSetAssignmentComponent(key);
|
const escapedKey = escapeCmdSetAssignmentComponent(key);
|
||||||
const escapedValue = escapeCmdSetAssignmentComponent(value);
|
const escapedValue = escapeCmdSetAssignmentComponent(value);
|
||||||
return `set "${escapedKey}=${escapedValue}"`;
|
return `set "${escapedKey}=${escapedValue}"`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
import { parseCmdScriptCommandLine, quoteCmdScriptArg } from "./cmd-argv.js";
|
||||||
import { parseCmdSetAssignment, renderCmdSetAssignment } from "./cmd-set.js";
|
import { assertNoCmdLineBreak, parseCmdSetAssignment, renderCmdSetAssignment } from "./cmd-set.js";
|
||||||
import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
import { resolveGatewayServiceDescription, resolveGatewayWindowsTaskName } from "./constants.js";
|
||||||
import { formatLine, writeFormattedLines } from "./output.js";
|
import { formatLine, writeFormattedLines } from "./output.js";
|
||||||
import { resolveGatewayStateDir } from "./paths.js";
|
import { resolveGatewayStateDir } from "./paths.js";
|
||||||
@@ -36,12 +36,8 @@ export function resolveTaskScriptPath(env: GatewayServiceEnv): string {
|
|||||||
return path.join(stateDir, scriptName);
|
return path.join(stateDir, scriptName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNoCmdLineBreak(value: string, field: string): void {
|
// `/TR` is parsed by schtasks itself, while the generated `gateway.cmd` line is parsed by cmd.exe.
|
||||||
if (/[\r\n]/.test(value)) {
|
// Keep their quoting strategies separate so each parser gets the encoding it expects.
|
||||||
throw new Error(`${field} cannot contain CR or LF in Windows task scripts.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function quoteSchtasksArg(value: string): string {
|
function quoteSchtasksArg(value: string): string {
|
||||||
if (!/[ \t"]/g.test(value)) {
|
if (!/[ \t"]/g.test(value)) {
|
||||||
return value;
|
return value;
|
||||||
@@ -49,22 +45,6 @@ function quoteSchtasksArg(value: string): string {
|
|||||||
return `"${value.replace(/"/g, '\\"')}"`;
|
return `"${value.replace(/"/g, '\\"')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function quoteCmdScriptArg(value: string): string {
|
|
||||||
assertNoCmdLineBreak(value, "Command argument");
|
|
||||||
if (!value) {
|
|
||||||
return '""';
|
|
||||||
}
|
|
||||||
const escaped = value.replace(/"/g, '\\"').replace(/%/g, "%%").replace(/!/g, "^!");
|
|
||||||
if (!/[ \t"&|<>^()%!]/g.test(value)) {
|
|
||||||
return escaped;
|
|
||||||
}
|
|
||||||
return `"${escaped}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unescapeCmdScriptArg(value: string): string {
|
|
||||||
return value.replace(/\^!/g, "!").replace(/%%/g, "%");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTaskUser(env: GatewayServiceEnv): string | null {
|
function resolveTaskUser(env: GatewayServiceEnv): string | null {
|
||||||
const username = env.USERNAME || env.USER || env.LOGNAME;
|
const username = env.USERNAME || env.USER || env.LOGNAME;
|
||||||
if (!username) {
|
if (!username) {
|
||||||
@@ -80,14 +60,6 @@ function resolveTaskUser(env: GatewayServiceEnv): string | null {
|
|||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCommandLine(value: string): string[] {
|
|
||||||
// `buildTaskScript` escapes quotes (`\"`) and cmd expansions (`%%`, `^!`).
|
|
||||||
// Keep all other backslashes literal so drive and UNC paths are preserved.
|
|
||||||
return splitArgsPreservingQuotes(value, { escapeMode: "backslash-quote-only" }).map(
|
|
||||||
unescapeCmdScriptArg,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readScheduledTaskCommand(
|
export async function readScheduledTaskCommand(
|
||||||
env: GatewayServiceEnv,
|
env: GatewayServiceEnv,
|
||||||
): Promise<GatewayServiceCommandConfig | null> {
|
): Promise<GatewayServiceCommandConfig | null> {
|
||||||
@@ -127,7 +99,7 @@ export async function readScheduledTaskCommand(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
programArguments: parseCommandLine(commandLine),
|
programArguments: parseCmdScriptCommandLine(commandLine),
|
||||||
...(workingDirectory ? { workingDirectory } : {}),
|
...(workingDirectory ? { workingDirectory } : {}),
|
||||||
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
||||||
};
|
};
|
||||||
@@ -180,8 +152,6 @@ function buildTaskScript({
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
assertNoCmdLineBreak(key, "Environment variable name");
|
|
||||||
assertNoCmdLineBreak(value, "Environment variable value");
|
|
||||||
lines.push(renderCmdSetAssignment(key, value));
|
lines.push(renderCmdSetAssignment(key, value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user