#!/usr/bin/env node const assert = require("assert"); const cp = require("child_process"); const crypto = require("crypto"); const fs = require("fs"); const net = require("net"); const path = require("path"); const { coordinatorWireRequest } = require("./coordinator-wire"); const { nodeIdentity, signedNodeRequest } = require("./node-signing"); const repo = path.resolve(__dirname, ".."); const forgejoWindowsNode = "forgejo-windows-node"; const forgejoWindowsIdentity = nodeIdentity("windows-runner-smoke", forgejoWindowsNode); const windowsEnvironmentDigest = `sha256:${crypto .createHash("sha256") .update("clusterflux/windows-command-dev/v1") .digest("hex")}`; const sourceSnapshot = `sha256:${crypto .createHash("sha256") .update("clusterflux/windows-runner/source/v1") .digest("hex")}`; class DapClient { constructor() { this.child = cp.spawn( "cargo", ["run", "-q", "-p", "clusterflux-dap", "--bin", "clusterflux-debug-dap"], { cwd: repo } ); this.seq = 1; this.buffer = Buffer.alloc(0); this.messages = []; this.waiters = []; this.stderr = ""; this.child.stdout.on("data", (chunk) => { this.buffer = Buffer.concat([this.buffer, chunk]); this.parse(); }); this.child.stderr.on("data", (chunk) => { this.stderr += chunk.toString(); }); this.child.on("exit", () => this.flushWaiters()); } send(command, args = {}) { const seq = this.seq++; const message = { seq, type: "request", command, arguments: args }; const payload = Buffer.from(JSON.stringify(coordinatorWireRequest(message))); this.child.stdin.write(`Content-Length: ${payload.length}\r\n\r\n`); this.child.stdin.write(payload); return seq; } async response(seq, command) { const message = await this.waitFor( (item) => item.type === "response" && item.request_seq === seq && item.command === command ); if (!message.success) { throw new Error(`DAP ${command} failed: ${message.message || JSON.stringify(coordinatorWireRequest(message))}`); } return message; } waitFor(predicate, timeoutMs = 120000) { const existing = this.messages.find(predicate); if (existing) return Promise.resolve(existing); return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.child.kill("SIGKILL"); reject(new Error(`timed out waiting for DAP message\n${this.stderr}`)); }, timeoutMs); this.waiters.push({ predicate, resolve, timer }); }); } parse() { while (true) { const headerEnd = this.buffer.indexOf("\r\n\r\n"); if (headerEnd < 0) return; const header = this.buffer.slice(0, headerEnd).toString(); const match = header.match(/Content-Length: (\d+)/i); if (!match) throw new Error(`bad DAP header: ${header}`); const length = Number(match[1]); const start = headerEnd + 4; const end = start + length; if (this.buffer.length < end) return; const payload = this.buffer.slice(start, end).toString(); this.buffer = this.buffer.slice(end); this.messages.push(JSON.parse(payload)); this.flushWaiters(); } } flushWaiters() { for (const waiter of [...this.waiters]) { const message = this.messages.find(waiter.predicate); if (!message) continue; clearTimeout(waiter.timer); this.waiters.splice(this.waiters.indexOf(waiter), 1); waiter.resolve(message); } } async close() { if (this.child.exitCode !== null) return; const seq = this.send("disconnect"); await this.response(seq, "disconnect"); this.child.stdin.end(); } } function waitForJsonLine(child) { return new Promise((resolve, reject) => { let buffer = ""; child.stdout.on("data", (chunk) => { buffer += chunk.toString(); const newline = buffer.indexOf("\n"); if (newline < 0) return; try { resolve(JSON.parse(buffer.slice(0, newline).trim())); } catch (error) { reject(error); } }); child.once("exit", (code) => { reject(new Error(`process exited before JSON line with code ${code}`)); }); }); } function send(addr, message) { return new Promise((resolve, reject) => { const socket = net.connect(addr.port, addr.host, () => { socket.write(`${JSON.stringify(coordinatorWireRequest(message))}\n`); }); let buffer = ""; socket.on("data", (chunk) => { buffer += chunk.toString(); const newline = buffer.indexOf("\n"); if (newline < 0) return; socket.end(); try { resolve(JSON.parse(buffer.slice(0, newline))); } catch (error) { reject(error); } }); socket.on("error", reject); }); } function runWindowsAttach(addr, grant) { return new Promise((resolve, reject) => { const child = cp.spawn( "cargo", [ "run", "-q", "-p", "clusterflux-cli", "--bin", "clusterflux", "--", "node", "attach", "--coordinator", `${addr.host}:${addr.port}`, "--tenant", "tenant", "--project-id", "project", "--node", forgejoWindowsNode, "--public-key", forgejoWindowsIdentity.publicKey, "--enrollment-grant", grant, "--cap", "windows-command-dev", "--json" ], { cwd: repo, env: { ...process.env, CLUSTERFLUX_NODE_PRIVATE_KEY: forgejoWindowsIdentity.privateKey, }, } ); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); child.on("exit", (code) => { if (code !== 0) { reject(new Error(`Windows node attach failed with code ${code}\n${stderr}`)); return; } try { resolve(JSON.parse(stdout)); } catch (error) { reject( new Error(`Windows node attach output was not JSON: ${stdout}\n${error.stack || error.message}`) ); } }); }); } function runWindowsNode(addr, grant) { return new Promise((resolve, reject) => { const child = cp.spawn( "cargo", [ "run", "-q", "-p", "clusterflux-node", "--bin", "clusterflux-node", "--", "--coordinator", `${addr.host}:${addr.port}`, "--tenant", "tenant", "--project-id", "project", "--node", forgejoWindowsNode, "--public-key", forgejoWindowsIdentity.publicKey, "--enrollment-grant", grant, "--process", "vp-forgejo-windows", "--task", "windows-command-dev", "--command", "cmd", "--arg", "/C", "--arg", "echo clusterflux-windows-runner", "--artifact", "/vfs/artifacts/windows-runner-output.txt" ], { cwd: repo, env: { ...process.env, CLUSTERFLUX_NODE_PRIVATE_KEY: forgejoWindowsIdentity.privateKey, }, } ); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); child.on("exit", (code) => { if (code !== 0) { reject(new Error(`Windows node smoke failed with code ${code}\n${stderr}`)); return; } try { resolve(JSON.parse(stdout.trim().split(/\r?\n/).at(-1))); } catch (error) { reject(new Error(`node output was not JSON: ${stdout}\n${error.stack || error.message}`)); } }); }); } async function assertDebuggerShowsWindowsThread() { const client = new DapClient(); try { const initialize = client.send("initialize", { adapterID: "clusterflux", linesStartAt1: true, columnsStartAt1: true }); await client.response(initialize, "initialize"); const launch = client.send("launch", { entry: "build", project: path.join(repo, "examples/launch-build-demo"), runtimeBackend: "simulated" }); await client.response(launch, "launch"); await client.waitFor( (message) => message.type === "event" && message.event === "initialized" ); const configurationDone = client.send("configurationDone"); await client.response(configurationDone, "configurationDone"); const threadsRequest = client.send("threads"); const threads = (await client.response(threadsRequest, "threads")).body.threads; assert( threads.some((thread) => thread.name.includes("compile windows")), "debugger must represent the Windows task as a virtual thread" ); await client.close(); } catch (error) { client.child.kill("SIGKILL"); throw error; } } (async () => { if (process.platform !== "win32") { throw new Error( `Windows runner validation requires win32; current platform is ${process.platform}` ); } const coordinator = cp.spawn( "cargo", [ "run", "-q", "-p", "clusterflux-coordinator", "--bin", "clusterflux-coordinator", "--", "--listen", "127.0.0.1:0", "--allow-local-trusted-loopback" ], { cwd: repo } ); let coordinatorStderr = ""; coordinator.stderr.on("data", (chunk) => { coordinatorStderr += chunk.toString(); }); try { const ready = await waitForJsonLine(coordinator); const [host, portText] = ready.listen.split(":"); const addr = { host, port: Number(portText) }; assert.strictEqual((await send(addr, { type: "ping" })).type, "pong"); const attachGrant = await send(addr, { type: "create_node_enrollment_grant", tenant: "tenant", project: "project", actor_user: "operator", ttl_seconds: 900 }); assert.strictEqual(attachGrant.type, "node_enrollment_grant_created"); assert.strictEqual(attachGrant.scope, "node:attach"); const attach = await runWindowsAttach(addr, attachGrant.grant); assert.strictEqual(attach.plan.node, forgejoWindowsNode); assert.strictEqual(attach.boundary.cli_contacted_coordinator, true); assert.strictEqual(attach.boundary.used_enrollment_exchange, true); assert.strictEqual(attach.coordinator_response.type, "node_enrollment_exchanged"); assert.strictEqual(attach.coordinator_response.credential.scope, "node:attach"); assert(attach.plan.capabilities.capabilities.includes("WindowsCommandDev")); const runtimeGrant = await send(addr, { type: "create_node_enrollment_grant", tenant: "tenant", project: "project", actor_user: "operator", ttl_seconds: 900 }); assert.strictEqual(runtimeGrant.type, "node_enrollment_grant_created"); const report = await runWindowsNode(addr, runtimeGrant.grant); assert.strictEqual(report.node_status, "completed"); assert.strictEqual(report.virtual_thread, "windows-command-dev"); assert.strictEqual(report.status_code, 0); assert.strictEqual(report.large_bytes_uploaded, false); assert.strictEqual( report.staged_artifact.path, "/vfs/artifacts/windows-runner-output.txt" ); assert.strictEqual(report.registration_response.type, "node_enrollment_exchanged"); assert.strictEqual(report.registration_response.credential.scope, "node:attach"); assert.strictEqual(report.coordinator_response.type, "task_recorded"); const recorded = await send(addr, signedNodeRequest(forgejoWindowsNode, forgejoWindowsIdentity, "report_node_capabilities", { type: "report_node_capabilities", tenant: "tenant", project: "project", node: forgejoWindowsNode, capabilities: { os: "Windows", arch: "x86_64", capabilities: ["Command", "WindowsCommandDev", "VfsArtifacts"], environment_backends: ["WindowsCommandDev"], source_providers: ["filesystem"] }, cached_environment_digests: [windowsEnvironmentDigest], source_snapshots: [sourceSnapshot], artifact_locations: ["windows-runner-output.txt"], direct_connectivity: true, online: true })); assert.strictEqual(recorded.type, "node_capabilities_recorded"); const placement = await send(addr, { type: "schedule_task", tenant: "tenant", project: "project", environment: { os: "Windows", arch: null, capabilities: ["WindowsCommandDev"] }, environment_digest: windowsEnvironmentDigest, required_capabilities: ["Command"], source_snapshot: sourceSnapshot, required_artifacts: ["windows-runner-output.txt"], prefer_node: null }); assert.strictEqual(placement.type, "task_placement"); assert.strictEqual(placement.placement.node, "forgejo-windows-node"); const link = await send(addr, { type: "create_artifact_download_link", tenant: "tenant", project: "project", actor_user: "user", artifact: "windows-runner-output.txt", max_bytes: 1024 }); assert.strictEqual(link.type, "artifact_download_link"); assert.deepStrictEqual(link.link.source, { RetainedNode: "forgejo-windows-node" }); } catch (error) { if (coordinatorStderr) { error.message = `${error.message}\ncoordinator stderr:\n${coordinatorStderr}`; } throw error; } finally { coordinator.kill("SIGTERM"); } await assertDebuggerShowsWindowsThread(); const outDir = path.join(repo, "target", "acceptance"); fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync( path.join(outDir, "windows-runner.json"), `${JSON.stringify( { kind: "clusterflux_windows_runner_validation", platform: process.platform, runner: process.env.FORGEJO_RUNNER_NAME || process.env.RUNNER_NAME || null, validated: true }, null, 2 )}\n` ); console.log("Windows runner smoke passed"); })().catch((error) => { console.error(error.stack || error.message); process.exit(1); });