feat: add outbound gateway demo
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { createServer } from "node:net";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const cwd = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/u, "$1");
|
||||
const useEdgeProxy = process.argv.includes("--edge");
|
||||
const cloudPort = await freePort();
|
||||
const gatewayPort = await freePort();
|
||||
const edgePort = useEdgeProxy ? await freePort() : null;
|
||||
const noProxy = mergeNoProxy(process.env.NO_PROXY, process.env.no_proxy, [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
]);
|
||||
const commonEnv = {
|
||||
...process.env,
|
||||
NO_PROXY: noProxy,
|
||||
no_proxy: noProxy,
|
||||
HWLAB_CLOUD_RUNTIME_ADAPTER: "memory",
|
||||
HWLAB_CLOUD_RUNTIME_DURABLE: "false"
|
||||
};
|
||||
const children = [];
|
||||
|
||||
try {
|
||||
const cloud = startNode("cmd/hwlab-cloud-api/main.mjs", {
|
||||
...commonEnv,
|
||||
HWLAB_CLOUD_API_HOST: "127.0.0.1",
|
||||
HWLAB_CLOUD_API_PORT: String(cloudPort),
|
||||
HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS: "10000"
|
||||
});
|
||||
children.push(cloud);
|
||||
await waitForJson(`http://127.0.0.1:${cloudPort}/health/live`);
|
||||
|
||||
if (useEdgeProxy) {
|
||||
const edge = startNode("cmd/hwlab-edge-proxy/main.mjs", {
|
||||
...commonEnv,
|
||||
HWLAB_EDGE_HOST: "127.0.0.1",
|
||||
HWLAB_EDGE_PORT: String(edgePort),
|
||||
HWLAB_EDGE_UPSTREAM: `http://127.0.0.1:${cloudPort}`
|
||||
});
|
||||
children.push(edge);
|
||||
await waitForJson(`http://127.0.0.1:${edgePort}/health`);
|
||||
}
|
||||
|
||||
const apiBaseUrl = useEdgeProxy ? `http://127.0.0.1:${edgePort}` : `http://127.0.0.1:${cloudPort}`;
|
||||
const gateway = startNode("cmd/hwlab-gateway/main.mjs", {
|
||||
...commonEnv,
|
||||
PORT: String(gatewayPort),
|
||||
HWLAB_GATEWAY_CLOUD_URL: apiBaseUrl,
|
||||
HWLAB_GATEWAY_ID: "gtw_local_demo",
|
||||
HWLAB_GATEWAY_SESSION_ID: "gws_gtw_local_demo",
|
||||
HWLAB_GATEWAY_CMD_EXEC_ENABLED: "1",
|
||||
HWLAB_GATEWAY_DEMO_OPEN: "1",
|
||||
HWLAB_GATEWAY_POLL_INTERVAL_MS: "100",
|
||||
HWLAB_GATEWAY_CMD_TIMEOUT_MS: "5000"
|
||||
});
|
||||
children.push(gateway);
|
||||
await waitForGatewaySession(apiBaseUrl, "gws_gtw_local_demo");
|
||||
|
||||
const rpc = await postJson(`${apiBaseUrl}/json-rpc`, {
|
||||
jsonrpc: "2.0",
|
||||
id: "req_gateway_outbound_demo_shell",
|
||||
method: "hardware.invoke.shell",
|
||||
params: {
|
||||
projectId: "prj_mvp_topology",
|
||||
gatewaySessionId: "gws_gtw_local_demo",
|
||||
resourceId: "res_windows_host",
|
||||
capabilityId: "cap_windows_cmd_exec",
|
||||
input: {
|
||||
command: "echo hwlab-demo",
|
||||
cwd,
|
||||
timeoutMs: 5000
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
traceId: "trc_gateway_outbound_demo_shell",
|
||||
actorId: "usr_gateway_outbound_demo",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
environment: "dev"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(rpc.error, undefined, JSON.stringify(rpc.error ?? {}, null, 2));
|
||||
assert.equal(rpc.result?.dispatch?.shellExecuted, true);
|
||||
assert.equal(rpc.result?.dispatch?.exitCode, 0);
|
||||
assert.match(rpc.result?.dispatch?.stdout ?? "", /hwlab-demo/u);
|
||||
console.log(JSON.stringify({
|
||||
status: "pass",
|
||||
mode: useEdgeProxy ? "edge-proxy" : "direct-cloud-api",
|
||||
cloudUrl: `http://127.0.0.1:${cloudPort}`,
|
||||
apiBaseUrl,
|
||||
gatewaySessionId: "gws_gtw_local_demo",
|
||||
stdout: rpc.result.dispatch.stdout.trim()
|
||||
}, null, 2));
|
||||
} finally {
|
||||
for (const child of children.reverse()) {
|
||||
stopChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
function startNode(entrypoint, env) {
|
||||
const child = spawn(process.execPath, [entrypoint], {
|
||||
cwd,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
});
|
||||
child.output = "";
|
||||
child.stderrOutput = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
child.output += chunk.toString("utf8");
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
child.stderrOutput += chunk.toString("utf8");
|
||||
});
|
||||
child.on("exit", (code, signal) => {
|
||||
child.exitInfo = { code, signal };
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
async function waitForGatewaySession(apiBaseUrl, gatewaySessionId) {
|
||||
const deadline = Date.now() + 8000;
|
||||
let last;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const body = await getJson(`${apiBaseUrl}/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 waitForJson(url) {
|
||||
const deadline = Date.now() + 8000;
|
||||
let last;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
return await getJson(url);
|
||||
} catch (error) {
|
||||
last = error;
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
throw new Error(`endpoint did not become ready: ${url}; last=${stringifyLast(last)}`);
|
||||
}
|
||||
|
||||
async function getJson(url) {
|
||||
const response = await fetch(url, {
|
||||
headers: { accept: "application/json" }
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
async function postJson(url, body) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
function freePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address ? address.port : null;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mergeNoProxy(...parts) {
|
||||
const values = new Set();
|
||||
for (const part of parts.flat()) {
|
||||
for (const item of String(part ?? "").split(",")) {
|
||||
const trimmed = item.trim();
|
||||
if (trimmed) values.add(trimmed);
|
||||
}
|
||||
}
|
||||
return [...values].join(",");
|
||||
}
|
||||
|
||||
function stopChild(child) {
|
||||
if (!child.killed && child.exitCode === null) {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyLast(value) {
|
||||
if (value instanceof Error) return value.message;
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
Reference in New Issue
Block a user