#!/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); }