Merge pull request #987 from pikasTech/fix/issue986-hwpod-node-command-resolution
fix: stabilize hwpod-node Windows git command resolution
This commit is contained in:
@@ -130,6 +130,8 @@ Keil MDK 装配规则:当 `spec.workspace.toolchain` 为 `keil-mdk`、`keil`
|
||||
| `io.uart.jsonrpc` | 通过 UART 做 JSON-RPC |
|
||||
| `cmd.run` | 临时维护命令透传,仅用于 `hwpod-ctl` 或早期薄 node 调试 |
|
||||
|
||||
`hwpod-node` 是 HWPOD 的唯一受控节点执行器。`cmd.run` 的命令解析、PATH 稳定性和跨平台差异必须在 `hwpod-node` 本体内处理;当 Windows service、后台进程或 D601 host 的环境变量与交互 shell 不一致时,不得用 gateway shell、手工 PowerShell、预先手工创建 worktree 或其他旁路代替 `hwpod-node-ops -> hwpod-node`。D601/Windows 实地调试应使用 UniDesk SSH 透传 `D601:win` 观察进程、PATH、工具安装位置和日志,但修复必须回到 `hwpod-node` 源码、配置或启动入口,并用 `/v1/hwpod-node-ops` 原链路复测。
|
||||
|
||||
标准 plan 形态:
|
||||
|
||||
```json
|
||||
|
||||
@@ -4,7 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { createHwpodNodeServer, executeHwpodNodeOpsPlan } from "./src/hwpod-node-lib.ts";
|
||||
import { createHwpodNodeServer, executeHwpodNodeOpsPlan, resolveHwpodNodeCommand } from "./src/hwpod-node-lib.ts";
|
||||
|
||||
test("hwpod-node executes minimal workspace node ops", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-"));
|
||||
@@ -95,6 +95,30 @@ test("hwpod-node reports cmd.run non-zero exits as failed results", async () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("hwpod-node resolves Git for Windows when service PATH omits git", async () => {
|
||||
const result = await resolveHwpodNodeCommand("git", {
|
||||
platform: "win32",
|
||||
env: {},
|
||||
fileExists: async (candidate: string) => candidate === "C:\\Program Files\\Git\\cmd\\git.exe"
|
||||
});
|
||||
|
||||
assert.equal(result.command, "C:\\Program Files\\Git\\cmd\\git.exe");
|
||||
assert.equal(result.resolved, true);
|
||||
assert.equal(result.source, "windows-well-known-tool");
|
||||
});
|
||||
|
||||
test("hwpod-node keeps explicit executable paths unchanged", async () => {
|
||||
const result = await resolveHwpodNodeCommand("C:\\Tools\\Git\\cmd\\git.exe", {
|
||||
platform: "win32",
|
||||
env: {},
|
||||
fileExists: async () => true
|
||||
});
|
||||
|
||||
assert.equal(result.command, "C:\\Tools\\Git\\cmd\\git.exe");
|
||||
assert.equal(result.resolved, false);
|
||||
assert.equal(result.source, "explicit-path");
|
||||
});
|
||||
|
||||
test("hwpod-node applies workspace patches through the stable node op", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-hwpod-node-patch-"));
|
||||
try {
|
||||
|
||||
@@ -436,7 +436,9 @@ async function spawnShellOutput(command: string, { cwd, timeoutMs }: any) {
|
||||
}
|
||||
|
||||
async function spawnOutput(command: string[], { cwd, stdinText = "", timeoutMs }: any) {
|
||||
const proc = Bun.spawn(command, { cwd, stdin: stdinText ? "pipe" : "ignore", stdout: "pipe", stderr: "pipe" });
|
||||
const resolution = await resolveHwpodNodeCommand(command[0]);
|
||||
const resolvedCommand = [resolution.command, ...command.slice(1)];
|
||||
const proc = Bun.spawn(resolvedCommand, { cwd, stdin: stdinText ? "pipe" : "ignore", stdout: "pipe", stderr: "pipe" });
|
||||
if (stdinText && proc.stdin) {
|
||||
proc.stdin.write(stdinText);
|
||||
proc.stdin.end();
|
||||
@@ -444,12 +446,56 @@ async function spawnOutput(command: string[], { cwd, stdinText = "", timeoutMs }
|
||||
const timeout = setTimeout(() => proc.kill(), timeoutMs);
|
||||
try {
|
||||
const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
|
||||
return { exitCode, stdout, stderr, ok: exitCode === 0 };
|
||||
return { exitCode, stdout, stderr, ok: exitCode === 0, commandResolution: resolution };
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveHwpodNodeCommand(command: string, options: any = {}) {
|
||||
const requested = requiredText(command, "command");
|
||||
const platform = text(options.platform) || process.platform;
|
||||
if (isExplicitExecutablePath(requested)) {
|
||||
return { requested, command: requested, resolved: false, source: "explicit-path" };
|
||||
}
|
||||
if (platform !== "win32") {
|
||||
return { requested, command: requested, resolved: false, source: "process-path" };
|
||||
}
|
||||
const candidates = windowsWellKnownCommandCandidates(requested, options.env ?? process.env);
|
||||
const exists = options.fileExists ?? fileExists;
|
||||
for (const candidate of candidates) {
|
||||
if (await exists(candidate)) {
|
||||
return { requested, command: candidate, resolved: true, source: "windows-well-known-tool", candidates };
|
||||
}
|
||||
}
|
||||
return { requested, command: requested, resolved: false, source: "process-path", candidates };
|
||||
}
|
||||
|
||||
function isExplicitExecutablePath(command: string) {
|
||||
return command.includes("/") || command.includes("\\") || path.isAbsolute(command) || isWindowsAbsolutePath(command);
|
||||
}
|
||||
|
||||
function windowsWellKnownCommandCandidates(command: string, env: Record<string, unknown>) {
|
||||
const normalized = command.toLowerCase().replace(/\.exe$/u, "");
|
||||
if (normalized !== "git") return [];
|
||||
return [
|
||||
text(env.HWPOD_NODE_GIT_EXE),
|
||||
"C:\\Program Files\\Git\\cmd\\git.exe",
|
||||
"C:\\Program Files\\Git\\bin\\git.exe",
|
||||
"C:\\Program Files (x86)\\Git\\cmd\\git.exe",
|
||||
"C:\\Program Files (x86)\\Git\\bin\\git.exe"
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
async function fileExists(candidate: string) {
|
||||
try {
|
||||
const info = await stat(candidate);
|
||||
return info.isFile() || !info.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function workspaceRoot(args: any) {
|
||||
const workspacePath = text(args.workspacePath) || process.env.HWPOD_WORKSPACE_PATH || process.cwd();
|
||||
return isWindowsAbsolutePath(workspacePath) ? normalizeWindowsPath(workspacePath) : path.resolve(workspacePath);
|
||||
|
||||
Reference in New Issue
Block a user