From f7e8138ab1c992b637e1c2886545f51575d42633 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Thu, 21 May 2026 20:19:40 +0300 Subject: [PATCH] feat: add windows helper bridge --- src/server/windowsHelper.js | 54 ++++++++++++++++++++++ test/server/windows-helper.test.js | 74 ++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/server/windowsHelper.js create mode 100644 test/server/windows-helper.test.js diff --git a/src/server/windowsHelper.js b/src/server/windowsHelper.js new file mode 100644 index 0000000..7571c31 --- /dev/null +++ b/src/server/windowsHelper.js @@ -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(); diff --git a/test/server/windows-helper.test.js b/test/server/windows-helper.test.js new file mode 100644 index 0000000..4db5cbb --- /dev/null +++ b/test/server/windows-helper.test.js @@ -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/, + ); +});