Files
pikasTech-HWLAB/scripts/gateway-outbound-nonblocking-probe.mjs
T
2026-05-25 03:34:43 +00:00

173 lines
5.4 KiB
JavaScript

#!/usr/bin/env node
import assert from "node:assert/strict";
import http from "node:http";
import https from "node:https";
import { setTimeout as delay } from "node:timers/promises";
const args = parseArgs(process.argv.slice(2));
const apiBaseUrl = stripTrailingSlash(args.apiBaseUrl || process.env.HWLAB_GATEWAY_PROBE_API_BASE_URL || "http://127.0.0.1:6667");
const gatewaySessionId = args.gatewaySessionId || process.env.HWLAB_GATEWAY_PROBE_SESSION_ID || "gws_gtw_local_demo";
const cwd = args.cwd || process.cwd();
const slowCommand = args.slowCommand || (process.platform === "win32" ? "ping -n 4 127.0.0.1 >NUL" : "sleep 3");
const quickCommand = args.quickCommand || "echo hwlab-nonblocking-quick";
const timeoutMs = positiveInteger(args.timeoutMs, 10000);
const quickMaxMs = positiveInteger(args.quickMaxMs, 2500);
await waitForGatewaySession();
const slow = postRpc({
id: "req_gateway_nonblocking_slow",
traceId: "trc_gateway_nonblocking_slow",
command: slowCommand
});
await delay(250);
const quickStartedAt = Date.now();
const quick = await postRpc({
id: "req_gateway_nonblocking_quick",
traceId: "trc_gateway_nonblocking_quick",
command: quickCommand
});
const quickDurationMs = Date.now() - quickStartedAt;
const slowResult = await slow;
assert.equal(quick.result?.dispatch?.shellExecuted, true, JSON.stringify(quick, null, 2));
assert.equal(quick.result?.dispatch?.exitCode, 0, JSON.stringify(quick, null, 2));
assert.equal(slowResult.result?.dispatch?.shellExecuted, true, JSON.stringify(slowResult, null, 2));
assert.equal(slowResult.result?.dispatch?.exitCode, 0, JSON.stringify(slowResult, null, 2));
assert.ok(quickDurationMs < quickMaxMs, `quick request was blocked by slow request for ${quickDurationMs}ms`);
console.log(JSON.stringify({
status: "pass",
apiBaseUrl,
gatewaySessionId,
quickDurationMs,
quickOperationId: quick.result?.operationId ?? null,
slowOperationId: slowResult.result?.operationId ?? null,
quickStdout: String(quick.result?.dispatch?.stdout ?? "").trim()
}, null, 2));
async function waitForGatewaySession() {
const deadline = Date.now() + 8000;
let last;
while (Date.now() < deadline) {
try {
const body = await getJson("/v1/gateway/sessions");
last = body;
const session = body.sessions?.find((item) => item.gatewaySessionId === gatewaySessionId);
if (session?.status === "online") return session;
} catch (error) {
last = error;
}
await delay(100);
}
throw new Error(`gateway session ${gatewaySessionId} did not become online: ${stringifyLast(last)}`);
}
async function getJson(path) {
return requestJson(`${apiBaseUrl}${path}`, {
method: "GET",
headers: { accept: "application/json" }
});
}
async function postRpc({ id, traceId, command }) {
return requestJson(`${apiBaseUrl}/json-rpc`, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json"
},
body: {
jsonrpc: "2.0",
id,
method: "hardware.invoke.shell",
params: {
projectId: "prj_mvp_topology",
gatewaySessionId,
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
input: {
command,
cwd,
timeoutMs
}
},
meta: {
traceId,
actorId: "usr_gateway_nonblocking_probe",
serviceId: "hwlab-cloud-api",
environment: "dev"
}
}
});
}
function requestJson(url, { method, headers = {}, body }) {
return new Promise((resolve, reject) => {
const target = new URL(url);
const client = target.protocol === "https:" ? https : http;
const payload = body === undefined ? "" : JSON.stringify(body);
const request = client.request(target, {
method,
headers: {
...headers,
...(payload ? { "content-length": Buffer.byteLength(payload) } : {})
}
}, (response) => {
let text = "";
response.setEncoding("utf8");
response.on("data", (chunk) => {
text += chunk;
});
response.on("end", () => {
if ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300) {
reject(new Error(`${method} ${target.pathname} HTTP ${response.statusCode}: ${text}`));
return;
}
try {
resolve(text ? JSON.parse(text) : null);
} catch (error) {
reject(new Error(`${method} ${target.pathname} returned non-JSON: ${error.message}`));
}
});
});
request.on("error", reject);
if (payload) request.write(payload);
request.end();
});
}
function parseArgs(argv) {
const result = {};
for (let index = 0; index < argv.length; index += 1) {
const item = argv[index];
if (item.startsWith("--") && item.includes("=")) {
const [key, ...rest] = item.slice(2).split("=");
result[toCamel(key)] = rest.join("=");
} else if (item.startsWith("--")) {
result[toCamel(item.slice(2))] = argv[index + 1] ?? "";
index += 1;
}
}
return result;
}
function toCamel(value) {
return String(value).replace(/-([a-z])/gu, (_, char) => char.toUpperCase());
}
function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function stripTrailingSlash(value) {
return String(value).replace(/\/+$/u, "");
}
function stringifyLast(value) {
if (value instanceof Error) return value.message;
return JSON.stringify(value);
}