173 lines
5.4 KiB
JavaScript
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);
|
|
}
|