feat: add windows helper bridge
This commit is contained in:
54
src/server/windowsHelper.js
Normal file
54
src/server/windowsHelper.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { settings } from "./config.js";
|
||||||
|
|
||||||
|
function defaultRunner(command, args, options = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
windowsHide: true,
|
||||||
|
});
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.on("data", (chunk) => {
|
||||||
|
stdout += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk.toString("utf8");
|
||||||
|
});
|
||||||
|
child.on("error", (error) => {
|
||||||
|
resolve({ status: 1, stdout, stderr: error.message });
|
||||||
|
});
|
||||||
|
child.on("close", (status) => {
|
||||||
|
resolve({ status, stdout, stderr });
|
||||||
|
});
|
||||||
|
child.stdin.end(options.input || "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWindowsHelper(options = {}) {
|
||||||
|
const helperPath = options.helperPath || settings.windowsHelperPath;
|
||||||
|
const command = options.command || "pwsh";
|
||||||
|
const runner = options.runner || defaultRunner;
|
||||||
|
return {
|
||||||
|
async run(action, payload = {}) {
|
||||||
|
const input = JSON.stringify({ action, payload });
|
||||||
|
const result = await runner(
|
||||||
|
command,
|
||||||
|
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", helperPath],
|
||||||
|
{ input },
|
||||||
|
);
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Windows helper failed: ${(result.stderr || result.stdout || "helper exited without stderr").trim()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(result.stdout);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Windows helper returned invalid JSON: ${result.stdout}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const windowsHelper = createWindowsHelper();
|
||||||
74
test/server/windows-helper.test.js
Normal file
74
test/server/windows-helper.test.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { createWindowsHelper } from "../../src/server/windowsHelper.js";
|
||||||
|
|
||||||
|
test("windows helper sends action and payload as JSON", async () => {
|
||||||
|
const calls = [];
|
||||||
|
const helper = createWindowsHelper({
|
||||||
|
helperPath: "scripts/windows/helper.ps1",
|
||||||
|
runner: async (command, args, options) => {
|
||||||
|
calls.push({ command, args, input: options.input });
|
||||||
|
return {
|
||||||
|
status: 0,
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
action: "status.get",
|
||||||
|
result: { proxifyre: "Running" },
|
||||||
|
}),
|
||||||
|
stderr: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helper.run("status.get", { service: "ProxiFyre" });
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
success: true,
|
||||||
|
action: "status.get",
|
||||||
|
result: { proxifyre: "Running" },
|
||||||
|
});
|
||||||
|
assert.equal(calls[0].command, "pwsh");
|
||||||
|
assert.deepEqual(calls[0].args, [
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-File",
|
||||||
|
"scripts/windows/helper.ps1",
|
||||||
|
]);
|
||||||
|
assert.deepEqual(JSON.parse(calls[0].input), {
|
||||||
|
action: "status.get",
|
||||||
|
payload: { service: "ProxiFyre" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("windows helper normalizes non-zero exit into structured error", async () => {
|
||||||
|
const helper = createWindowsHelper({
|
||||||
|
helperPath: "scripts/windows/helper.ps1",
|
||||||
|
runner: async () => ({
|
||||||
|
status: 1,
|
||||||
|
stdout: "",
|
||||||
|
stderr: "service failed",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => helper.run("service.restart", { name: "proxifyre" }),
|
||||||
|
/Windows helper failed: service failed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("windows helper rejects invalid JSON stdout", async () => {
|
||||||
|
const helper = createWindowsHelper({
|
||||||
|
helperPath: "scripts/windows/helper.ps1",
|
||||||
|
runner: async () => ({
|
||||||
|
status: 0,
|
||||||
|
stdout: "not-json",
|
||||||
|
stderr: "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => helper.run("status.get", {}),
|
||||||
|
/Windows helper returned invalid JSON/,
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user