fix: make device pod host cli launch windows safe

This commit is contained in:
Codex
2026-05-29 15:58:55 +08:00
parent aee27770ed
commit 6c78f34158
3 changed files with 62 additions and 13 deletions
+9 -9
View File
@@ -222,7 +222,8 @@ test("device pod executor dispatches internal jobs through cloud-api gateway ada
assert.equal(dispatches[0].internalService, "hwlab-device-pod");
assert.equal(dispatches[0].body.params.gatewaySessionId, "gws_devicepod_test");
assert.equal(dispatches[0].body.params.input.cwd, "F:\\Work\\Project");
assert.match(dispatches[0].body.params.input.command, /^node -e /u);
assert.match(dispatches[0].body.params.input.command, /^node tools\\device-host-cli\.mjs --profile-json-b64 /u);
assert.equal(dispatches[0].body.params.input.command.includes("node -e"), false);
} finally {
await service.stop();
await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve())));
@@ -269,15 +270,14 @@ test("device-pod executor maps v0.1 CLI options to device-host-cli argv", async
assert.equal(jobResponse.status, 202);
await waitForJobStatus(service.port, "device-pod-test", "job_options_test", "completed");
const command = dispatchedBody.params.input.command;
const encoded = command.match(/"([A-Za-z0-9+/=]+)"$/u)[1];
assert.equal(command.includes("node -e"), false);
assert.match(command, /^node tools\/device-host-cli\.mjs --profile-json-b64 /u);
assert.match(command, / --pod-id device-pod-test debug-probe download start /u);
assert.match(command, / --capture-uart uart\/1 --capture-duration-ms 8000 --port COM4 --baud-rate 921600$/u);
const encoded = command.match(/--profile-json-b64 ([A-Za-z0-9+/=]+)/u)[1];
const decoded = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
assert.deepEqual(decoded.args, [
"debug-probe", "download", "start",
"--capture-uart", "uart/1",
"--capture-duration-ms", "8000",
"--port", "COM4",
"--baud-rate", "921600"
]);
assert.equal(decoded.devicePodId, "device-pod-test");
assert.equal(decoded.route.gatewaySessionId, "gws_test");
} finally {
await service.stop();
await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve())));
+35 -4
View File
@@ -383,13 +383,44 @@ function deviceHostCommand(profile, job) {
const payload = {
devicePodId: profile.devicePodId || job.devicePodId,
profile,
profilePath: `.device-pod/.runtime/${job.id}.json`,
hostCli: profile.route?.hostCli || "node tools/device-host-cli.mjs",
args
};
const runner = "const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');const p=JSON.parse(Buffer.from(process.argv[1],'base64').toString('utf8'));fs.mkdirSync(path.dirname(p.profilePath),{recursive:true});fs.writeFileSync(p.profilePath,JSON.stringify(p.profile,null,2)+'\\n','utf8');function split(s){const out=[];let q=null,b='';for(const ch of String(s)){if(q){if(ch===q)q=null;else b+=ch}else if(ch.charCodeAt(0)===39||ch.charCodeAt(0)===34)q=ch;else if(/\\s/u.test(ch)){if(b){out.push(b);b=''}}else b+=ch}if(b)out.push(b);return out}const parts=split(p.hostCli||'node tools/device-host-cli.mjs');const child=cp.spawn(parts[0],parts.slice(1).concat(['--profile',p.profilePath,'--pod-id',p.devicePodId],p.args),{stdio:'inherit',shell:false});child.on('exit',(c)=>process.exit(c??0));child.on('error',(e)=>{console.error(e.message);process.exit(127)});";
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return `node -e "${runner}" "${encoded}"`;
const profileJsonB64 = Buffer.from(JSON.stringify(payload.profile), "utf8").toString("base64");
return shellJoin([...splitCommand(payload.hostCli), "--profile-json-b64", profileJsonB64, "--pod-id", payload.devicePodId, ...payload.args]);
}
function splitCommand(value) {
const out = [];
let quote = null;
let buffer = "";
for (const char of String(value || "")) {
if (quote) {
if (char === quote) quote = null;
else buffer += char;
} else if (char === "'" || char === '"') {
quote = char;
} else if (/\s/u.test(char)) {
if (buffer) {
out.push(buffer);
buffer = "";
}
} else {
buffer += char;
}
}
if (buffer) out.push(buffer);
return out.length ? out : ["node", "tools/device-host-cli.mjs"];
}
function shellJoin(parts) {
return parts.map(shellQuote).join(" ");
}
function shellQuote(value) {
const text = String(value ?? "");
if (/^[A-Za-z0-9_./:=@\\-]+$/u.test(text)) return text;
return `"${text.replace(/(["\\])/gu, "\\$1")}"`;
}
function deviceHostArgs(intent, args = {}) {
@@ -20,6 +20,11 @@ function parseGlobalCli(argv) {
i += 1;
} else if (item.startsWith('--profile=')) {
options.profile = cleanArg(item.slice('--profile='.length));
} else if (item === '--profile-json-b64') {
options.profileJsonB64 = cleanArg(argv[i + 1]);
i += 1;
} else if (item.startsWith('--profile-json-b64=')) {
options.profileJsonB64 = cleanArg(item.slice('--profile-json-b64='.length));
} else if (item === '--pod-id') {
options.podId = cleanArg(argv[i + 1]);
i += 1;
@@ -93,6 +98,7 @@ function profilePath(root = workspaceRoot(), podId = selectedPodId()) {
function loadProfile() {
const root = workspaceRoot();
if (GLOBAL_CLI.options.profileJsonB64) return loadInlineProfile(root);
const requestedProfile = GLOBAL_CLI.options.profile || process.env.DEVICE_POD_PROFILE || '';
const file = requestedProfile ? (path.isAbsolute(requestedProfile) ? requestedProfile : path.resolve(root, requestedProfile)) : profilePath(root);
if (!fs.existsSync(file)) throw new Error(`device-pod profile not found: ${file}`);
@@ -105,6 +111,18 @@ function loadProfile() {
return profile;
}
function loadInlineProfile(root) {
const profile = JSON.parse(Buffer.from(GLOBAL_CLI.options.profileJsonB64, 'base64').toString('utf8'));
const actualPodId = profile.devicePodId || profile.podId;
const requestedPodId = GLOBAL_CLI.options.podId || process.env.DEVICE_POD_ID || '';
if (requestedPodId && actualPodId && actualPodId !== requestedPodId) throw new Error(`device-pod profile mismatch: requested ${requestedPodId}, profile ${actualPodId}`);
const runtimePath = path.join(devicePodDir(root), '.runtime', `${actualPodId || requestedPodId || DEFAULT_POD_ID}.json`);
writeJson(runtimePath, profile);
profile.__profilePath = runtimePath;
profile.__workspaceRoot = path.resolve(profile.workspaceRoot || root);
return profile;
}
function resolveWorkspacePath(profile, rel = '.') {
const root = profile.__workspaceRoot;
const raw = String(rel || '.').replace(/^\/+/, '');