Skip to content

Commit ff26dc2

Browse files
feat: add new unsafeInspectorProxy option to miniflare (#8357)
--------- Co-authored-by: Pete Bacon Darwin <[email protected]>
1 parent 03435cc commit ff26dc2

File tree

11 files changed

+1071
-6
lines changed

11 files changed

+1071
-6
lines changed

.changeset/little-spiders-sell.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
feat: add new `unsafeInspectorProxy` option to miniflare
6+
7+
Add a new `unsafeInspectorProxy` option to the miniflare worker options, if
8+
at least one worker has the option set then miniflare will establish a proxy
9+
between itself and workerd for the v8 inspector APIs which exposes only the
10+
requested workers to inspector clients. The inspector proxy communicates through
11+
miniflare's `inspectorPort` and exposes each requested worker via a path comprised
12+
of the worker's name
13+
14+
example:
15+
16+
```js
17+
import { Miniflare } from "miniflare";
18+
19+
const mf = new Miniflare({
20+
// the inspector proxy will be accessible through port 9229
21+
inspectorPort: 9229,
22+
workers: [
23+
{
24+
name: "worker-a",
25+
scriptPath: "./worker-a.js",
26+
// enable the inspector proxy for worker-a
27+
unsafeInspectorProxy: true,
28+
},
29+
{
30+
name: "worker-b",
31+
scriptPath: "./worker-b.js",
32+
// worker-b is not going to be proxied
33+
},
34+
{
35+
name: "worker-c",
36+
scriptPath: "./worker-c.js",
37+
// enable the inspector proxy for worker-c
38+
unsafeInspectorProxy: true,
39+
},
40+
],
41+
});
42+
```
43+
44+
In the above example an inspector proxy gets set up which exposes `worker-a` and `worker-b`,
45+
inspector clients can discover such workers via `http://localhost:9229` and communicate with
46+
them respectively via `ws://localhost:9229/worker-a` and `ws://localhost:9229/worker-b`
47+
48+
Note: this API is experimental, thus it's not being added to the public documentation and
49+
it's prefixed by `unsafe`

packages/miniflare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"eslint-plugin-es": "^4.1.0",
8484
"eslint-plugin-prettier": "^5.0.1",
8585
"expect-type": "^0.15.0",
86+
"get-port": "^7.1.0",
8687
"heap-js": "^2.5.0",
8788
"http-cache-semantics": "^4.1.0",
8889
"kleur": "^4.1.5",

packages/miniflare/src/index.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
reviveError,
7979
ServiceDesignatorSchema,
8080
} from "./plugins/core";
81+
import { InspectorProxyController } from "./plugins/core/inspector-proxy";
8182
import {
8283
Config,
8384
Extension,
@@ -744,12 +745,32 @@ export class Miniflare {
744745
readonly #webSocketServer: WebSocketServer;
745746
readonly #webSocketExtraHeaders: WeakMap<http.IncomingMessage, Headers>;
746747

748+
#maybeInspectorProxyController?: InspectorProxyController;
749+
#previousRuntimeInspectorPort?: number;
750+
747751
constructor(opts: MiniflareOptions) {
748752
// Split and validate options
749753
const [sharedOpts, workerOpts] = validateOptions(opts);
750754
this.#sharedOpts = sharedOpts;
751755
this.#workerOpts = workerOpts;
752756

757+
const workerNamesToProxy = new Set(
758+
this.#workerOpts
759+
.filter(({ core: { unsafeInspectorProxy } }) => !!unsafeInspectorProxy)
760+
.map((w) => w.core.name ?? "")
761+
);
762+
763+
const enableInspectorProxy = workerNamesToProxy.size > 0;
764+
765+
if (enableInspectorProxy) {
766+
if (this.#sharedOpts.core.inspectorPort === undefined) {
767+
throw new MiniflareCoreError(
768+
"ERR_MISSING_INSPECTOR_PROXY_PORT",
769+
"inspector proxy requested but without an inspectorPort specified"
770+
);
771+
}
772+
}
773+
753774
// Add to registry after initial options validation, before any servers/
754775
// child processes are started
755776
if (maybeInstanceRegistry !== undefined) {
@@ -760,6 +781,21 @@ export class Miniflare {
760781

761782
this.#log = this.#sharedOpts.core.log ?? new NoOpLog();
762783

784+
if (enableInspectorProxy) {
785+
if (this.#sharedOpts.core.inspectorPort === undefined) {
786+
throw new MiniflareCoreError(
787+
"ERR_MISSING_INSPECTOR_PROXY_PORT",
788+
"inspector proxy requested but without an inspectorPort specified"
789+
);
790+
}
791+
792+
this.#maybeInspectorProxyController = new InspectorProxyController(
793+
this.#sharedOpts.core.inspectorPort,
794+
this.#log,
795+
workerNamesToProxy
796+
);
797+
}
798+
763799
this.#liveReloadServer = new WebSocketServer({ noServer: true });
764800
this.#webSocketServer = new WebSocketServer({
765801
noServer: true,
@@ -1406,14 +1442,21 @@ export class Miniflare {
14061442
configuredHost,
14071443
this.#sharedOpts.core.port
14081444
);
1409-
let inspectorAddress: string | undefined;
1445+
let runtimeInspectorAddress: string | undefined;
14101446
if (this.#sharedOpts.core.inspectorPort !== undefined) {
1411-
inspectorAddress = this.#getSocketAddress(
1447+
let runtimeInspectorPort = this.#sharedOpts.core.inspectorPort;
1448+
if (this.#maybeInspectorProxyController !== undefined) {
1449+
// if we have an inspector proxy let's use a
1450+
// random port for the actual runtime inspector
1451+
runtimeInspectorPort = 0;
1452+
}
1453+
runtimeInspectorAddress = this.#getSocketAddress(
14121454
kInspectorSocket,
1413-
this.#previousSharedOpts?.core.inspectorPort,
1455+
this.#previousRuntimeInspectorPort,
14141456
"localhost",
1415-
this.#sharedOpts.core.inspectorPort
1457+
runtimeInspectorPort
14161458
);
1459+
this.#previousRuntimeInspectorPort = runtimeInspectorPort;
14171460
}
14181461
const loopbackAddress = `${
14191462
maybeGetLocallyAccessibleHost(configuredHost) ??
@@ -1424,7 +1467,7 @@ export class Miniflare {
14241467
entryAddress,
14251468
loopbackAddress,
14261469
requiredSockets,
1427-
inspectorAddress,
1470+
inspectorAddress: runtimeInspectorAddress,
14281471
verbose: this.#sharedOpts.core.verbose,
14291472
handleRuntimeStdio: this.#sharedOpts.core.handleRuntimeStdio,
14301473
};
@@ -1445,6 +1488,19 @@ export class Miniflare {
14451488
// all of `requiredSockets` as keys.
14461489
this.#socketPorts = maybeSocketPorts;
14471490

1491+
if (this.#maybeInspectorProxyController !== undefined) {
1492+
// Try to get inspector port for the workers
1493+
const maybePort = this.#socketPorts.get(kInspectorSocket);
1494+
if (maybePort === undefined) {
1495+
throw new MiniflareCoreError(
1496+
"ERR_RUNTIME_FAILURE",
1497+
"Unable to access the runtime inspector socket."
1498+
);
1499+
} else {
1500+
this.#maybeInspectorProxyController.updateConnection(maybePort);
1501+
}
1502+
}
1503+
14481504
const entrySocket = config.sockets?.[0];
14491505
const secure = entrySocket !== undefined && "https" in entrySocket;
14501506
const previousEntryURL = this.#runtimeEntryURL;
@@ -1527,6 +1583,8 @@ export class Miniflare {
15271583
// `dispose()`d synchronously, immediately after constructing a `Miniflare`
15281584
// instance. In this case, return a discard URL which we'll ignore.
15291585
if (disposing) return new URL("http://[100::]/");
1586+
// if there is an inspector proxy let's wait for it to be ready
1587+
await this.#maybeInspectorProxyController?.ready;
15301588
// Make sure `dispose()` wasn't called in the time we've been waiting
15311589
this.#checkDisposed();
15321590
// `#runtimeEntryURL` is assigned in `#assembleAndUpdateConfig()`, which is
@@ -1551,6 +1609,10 @@ export class Miniflare {
15511609
this.#checkDisposed();
15521610
await this.ready;
15531611

1612+
if (this.#maybeInspectorProxyController !== undefined) {
1613+
return this.#maybeInspectorProxyController.getInspectorURL();
1614+
}
1615+
15541616
// `#socketPorts` is assigned in `#assembleAndUpdateConfig()`, which is
15551617
// called by `#init()`, and `ready` doesn't resolve until `#init()` returns
15561618
assert(this.#socketPorts !== undefined);
@@ -1900,6 +1962,9 @@ export class Miniflare {
19001962
// `rm -rf ${#tmpPath}`, this won't throw if `#tmpPath` doesn't exist
19011963
await fs.promises.rm(this.#tmpPath, { force: true, recursive: true });
19021964

1965+
// Close the inspector proxy server if there is one
1966+
await this.#maybeInspectorProxyController?.dispose();
1967+
19031968
// Remove from instance registry as last step in `finally`, to make sure
19041969
// all dispose steps complete
19051970
maybeInstanceRegistry?.delete(this);

packages/miniflare/src/plugins/core/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ const CoreOptionsSchemaInput = z.intersection(
126126
compatibilityDate: z.string().optional(),
127127
compatibilityFlags: z.string().array().optional(),
128128

129+
unsafeInspectorProxy: z.boolean().optional(),
130+
129131
routes: z.string().array().optional(),
130132

131133
bindings: z.record(JsonSchema).optional(),
@@ -191,6 +193,7 @@ export const CoreSharedOptionsSchema = z.object({
191193
httpsCertPath: z.string().optional(),
192194

193195
inspectorPort: z.number().optional(),
196+
194197
verbose: z.boolean().optional(),
195198

196199
log: z.instanceof(Log).optional(),
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type Protocol from "devtools-protocol/types/protocol-mapping";
2+
3+
type _Params<ParamsArray extends [unknown?]> = ParamsArray extends [infer P]
4+
? P
5+
: undefined;
6+
7+
type _EventMethods = keyof Protocol.Events;
8+
export type DevToolsEvent<Method extends _EventMethods> = Method extends unknown
9+
? {
10+
method: Method;
11+
params: _Params<Protocol.Events[Method]>;
12+
}
13+
: never;
14+
15+
export type DevToolsEvents = DevToolsEvent<_EventMethods>;
16+
17+
type _CommandMethods = keyof Protocol.Commands;
18+
export type DevToolsCommandRequest<Method extends _CommandMethods> =
19+
Method extends unknown
20+
? _Params<Protocol.Commands[Method]["paramsType"]> extends undefined
21+
? {
22+
id: number;
23+
method: Method;
24+
}
25+
: {
26+
id: number;
27+
method: Method;
28+
params: _Params<Protocol.Commands[Method]["paramsType"]>;
29+
}
30+
: never;
31+
32+
export type DevToolsCommandRequests = DevToolsCommandRequest<_CommandMethods>;
33+
34+
export type DevToolsCommandResponse<Method extends _CommandMethods> =
35+
Method extends unknown
36+
? {
37+
id: number;
38+
result: Protocol.Commands[Method]["returnType"];
39+
}
40+
: never;
41+
export type DevToolsCommandResponses = DevToolsCommandResponse<_CommandMethods>;
42+
43+
export function isDevToolsEvent<
44+
Method extends DevToolsEvent<_EventMethods>["method"],
45+
>(event: unknown, name: Method): event is DevToolsEvent<Method> {
46+
return (
47+
typeof event === "object" &&
48+
event !== null &&
49+
"method" in event &&
50+
event.method === name
51+
);
52+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { InspectorProxyController } from "./inspector-proxy-controller";

0 commit comments

Comments
 (0)