From ab18916fa40cba8d91c70bd44cba48b18d110e1c Mon Sep 17 00:00:00 2001 From: bcoll Date: Tue, 12 Sep 2023 10:09:13 +0100 Subject: [PATCH] Add `Miniflare#getInspectorURL()` for getting inspector base URL Allows setting `inspectorPort: 0` to get a random inspector port. We'd like this for Wrangler's inspector proxy, so we don't have to use `get-port` and can rely on the OS to give us a random port. --- packages/miniflare/src/index.ts | 29 +++++++++++- packages/miniflare/src/runtime/index.ts | 27 +++++++---- .../test/plugins/core/errors/index.spec.ts | 47 +++++++------------ 3 files changed, 63 insertions(+), 40 deletions(-) diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index fd84aead3..d4fe4d379 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -81,6 +81,7 @@ import { SocketIdentifier, Worker_Binding, Worker_Module, + kInspectorSocket, serializeConfig, } from "./runtime"; import { @@ -990,7 +991,11 @@ export class Miniflare { return name; } ); - + // TODO(now): there's a bug here if the inspector was not enabled initially, + // fixed by a later commit in this PR + if (this.#sharedOpts.core.inspectorPort !== undefined) { + requiredSockets.push(kInspectorSocket); + } const maybeSocketPorts = await this.#runtime.updateConfig( configBuffer, requiredSockets, @@ -1088,7 +1093,27 @@ export class Miniflare { return this.#waitForReady(); } - async unsafeGetDirectURL(workerName?: string) { + async getInspectorURL(): Promise { + this.#checkDisposed(); + await this.ready; + + // `#socketPorts` is assigned in `#assembleAndUpdateConfig()`, which is + // called by `#init()`, and `ready` doesn't resolve until `#init()` returns + assert(this.#socketPorts !== undefined); + + // Try to get inspector port for worker + const maybePort = this.#socketPorts.get(kInspectorSocket); + if (maybePort === undefined) { + throw new TypeError( + "Inspector not enabled in Miniflare instance. " + + "Set the `inspectorPort` option to enable it." + ); + } + + return new URL(`ws://127.0.0.1:${maybePort}`); + } + + async unsafeGetDirectURL(workerName?: string): Promise { this.#checkDisposed(); await this.ready; diff --git a/packages/miniflare/src/runtime/index.ts b/packages/miniflare/src/runtime/index.ts index e1febda62..d457c25f5 100644 --- a/packages/miniflare/src/runtime/index.ts +++ b/packages/miniflare/src/runtime/index.ts @@ -11,13 +11,20 @@ import { z } from "zod"; import { SERVICE_LOOPBACK, SOCKET_ENTRY } from "../plugins"; import { Awaitable } from "../workers"; -const ControlMessageSchema = z.object({ - event: z.literal("listen"), - socket: z.string(), - port: z.number(), -}); - -export type SocketIdentifier = string; +const ControlMessageSchema = z.discriminatedUnion("event", [ + z.object({ + event: z.literal("listen"), + socket: z.string(), + port: z.number(), + }), + z.object({ + event: z.literal("listen-inspector"), + port: z.number(), + }), +]); + +export const kInspectorSocket = Symbol("kInspectorSocket"); +export type SocketIdentifier = string | typeof kInspectorSocket; async function waitForPorts( requiredSockets: SocketIdentifier[], @@ -37,12 +44,14 @@ async function waitForPorts( const message = ControlMessageSchema.safeParse(JSON.parse(line)); // If this was an unrecognised control message, ignore it if (!message.success) continue; - const socket = message.data.socket; + const data = message.data; + const socket: SocketIdentifier = + data.event === "listen-inspector" ? kInspectorSocket : data.socket; const index = requiredSockets.indexOf(socket); // If this wasn't a required socket, ignore it if (index === -1) continue; // Record the port of this socket - socketPorts.set(socket, message.data.port); + socketPorts.set(socket, data.port); // Satisfy the requirement, if there are no more, return the ports map requiredSockets.splice(index, 1); if (requiredSockets.length === 0) return socketPorts; diff --git a/packages/miniflare/test/plugins/core/errors/index.spec.ts b/packages/miniflare/test/plugins/core/errors/index.spec.ts index 03a15f16f..2820218a8 100644 --- a/packages/miniflare/test/plugins/core/errors/index.spec.ts +++ b/packages/miniflare/test/plugins/core/errors/index.spec.ts @@ -1,7 +1,5 @@ import assert from "assert"; import fs from "fs/promises"; -import http from "http"; -import { AddressInfo } from "net"; import path from "path"; import { fileURLToPath } from "url"; import test from "ava"; @@ -48,18 +46,8 @@ test("source maps workers", async (t) => { const serviceWorkerContent = await fs.readFile(serviceWorkerPath, "utf8"); const modulesContent = await fs.readFile(modulesPath, "utf8"); - // The OS should assign random ports in sequential order, meaning - // `inspectorPort` is unlikely to be immediately chosen as a random port again - const server = http.createServer(); - const inspectorPort = await new Promise((resolve, reject) => { - server.listen(0, () => { - const port = (server.address() as AddressInfo).port; - server.close((err) => (err ? reject(err) : resolve(port))); - }); - }); - const mf = new Miniflare({ - inspectorPort, + inspectorPort: 0, workers: [ { bindings: { MESSAGE: "unnamed" }, @@ -190,23 +178,24 @@ addEventListener("fetch", (event) => { t.regex(String(error?.stack), nestedRegexp); // Check source mapping URLs rewritten - let sources = await getSources(inspectorPort, "core:user:"); + const inspectorBaseURL = await mf.getInspectorURL(); + let sources = await getSources(inspectorBaseURL, "core:user:"); t.deepEqual(sources, [REDUCE_PATH, SERVICE_WORKER_ENTRY_PATH]); - sources = await getSources(inspectorPort, "core:user:a"); + sources = await getSources(inspectorBaseURL, "core:user:a"); t.deepEqual(sources, [REDUCE_PATH, SERVICE_WORKER_ENTRY_PATH]); - sources = await getSources(inspectorPort, "core:user:b"); + sources = await getSources(inspectorBaseURL, "core:user:b"); t.deepEqual(sources, [MODULES_ENTRY_PATH, REDUCE_PATH]); - sources = await getSources(inspectorPort, "core:user:c"); + sources = await getSources(inspectorBaseURL, "core:user:c"); t.deepEqual(sources, [MODULES_ENTRY_PATH, REDUCE_PATH]); - sources = await getSources(inspectorPort, "core:user:d"); + sources = await getSources(inspectorBaseURL, "core:user:d"); t.deepEqual(sources, [MODULES_ENTRY_PATH, REDUCE_PATH]); - sources = await getSources(inspectorPort, "core:user:e"); + sources = await getSources(inspectorBaseURL, "core:user:e"); t.deepEqual(sources, [MODULES_ENTRY_PATH, REDUCE_PATH]); - sources = await getSources(inspectorPort, "core:user:f"); + sources = await getSources(inspectorBaseURL, "core:user:f"); t.deepEqual(sources, [MODULES_ENTRY_PATH, REDUCE_PATH]); - sources = await getSources(inspectorPort, "core:user:g"); + sources = await getSources(inspectorBaseURL, "core:user:g"); t.deepEqual(sources, [MODULES_ENTRY_PATH, REDUCE_PATH]); - sources = await getSources(inspectorPort, "core:user:h"); + sources = await getSources(inspectorBaseURL, "core:user:h"); t.deepEqual(sources, [DEP_ENTRY_PATH, REDUCE_PATH]); // (entry point script overridden) // Check respects map's existing `sourceRoot` @@ -217,7 +206,7 @@ addEventListener("fetch", (event) => { ); serviceWorkerMap.sourceRoot = sourceRoot; await fs.writeFile(serviceWorkerMapPath, JSON.stringify(serviceWorkerMap)); - t.deepEqual(await getSources(inspectorPort, "core:user:"), [ + t.deepEqual(await getSources(inspectorBaseURL, "core:user:"), [ path.resolve(tmp, sourceRoot, path.relative(tmp, REDUCE_PATH)), path.resolve( tmp, @@ -227,18 +216,18 @@ addEventListener("fetch", (event) => { ]); // Check does nothing with URL source mapping URLs - const sourceMapURL = await getSourceMapURL(inspectorPort, "core:user:i"); + const sourceMapURL = await getSourceMapURL(inspectorBaseURL, "core:user:i"); t.regex(sourceMapURL, /^data:application\/json;base64/); }); function getSourceMapURL( - inspectorPort: number, + inspectorBaseURL: URL, serviceName: string ): Promise { let sourceMapURL: string | undefined; const promise = new DeferredPromise(); - const inspectorUrl = `ws://127.0.0.1:${inspectorPort}/${serviceName}`; - const ws = new NodeWebSocket(inspectorUrl); + const inspectorURL = new URL(`/${serviceName}`, inspectorBaseURL); + const ws = new NodeWebSocket(inspectorURL); ws.on("message", async (raw) => { try { const message = JSON.parse(raw.toString("utf8")); @@ -264,8 +253,8 @@ function getSourceMapURL( return promise; } -async function getSources(inspectorPort: number, serviceName: string) { - const sourceMapURL = await getSourceMapURL(inspectorPort, serviceName); +async function getSources(inspectorBaseURL: URL, serviceName: string) { + const sourceMapURL = await getSourceMapURL(inspectorBaseURL, serviceName); assert(sourceMapURL.startsWith("file:")); const sourceMapPath = fileURLToPath(sourceMapURL); const sourceMapData = await fs.readFile(sourceMapPath, "utf8");