Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 32db23b

Browse files
authored
Fix WSL support, closes cloudflare/workers-sdk#2243 (#443)
Previously, Miniflare passed Windows paths to WSL, without first converting them to WSL-friendly `/mnt/...` paths. This PR fixes that, WSL detection, and also adds a new restart script similar to the Docker runtime to massively reduce script-reload time.
1 parent 0e2924d commit 32db23b

File tree

4 files changed

+148
-27
lines changed

4 files changed

+148
-27
lines changed

packages/tre/lib/restart.sh renamed to packages/tre/lib/docker-restart.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env bash
22
# Restarts a process on receiving SIGUSR1.
33
# Usage: ./restart.sh <command> <...args>
4+
set -eo pipefail
45

56
# Start process and record its PID
67
"$@" &

packages/tre/lib/wsl-restart.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
# Wraps a process with commands for restarting/stopping.
3+
# We can't use signals here as this script will be started by wsl.exe
4+
# which doesn't pass them through correctly.
5+
# Usage: ./wsl-restart.sh <command> <...args>
6+
set -eo pipefail
7+
8+
# Start process and log its PID
9+
"$@" &
10+
PID=$!
11+
echo "[*] Started $PID"
12+
13+
while read LINE
14+
do
15+
if [[ $LINE = "restart" ]]; then
16+
# Kill existing process...
17+
kill -TERM $PID
18+
# ...and start a new one
19+
"$@" &
20+
PID=$!
21+
echo "[*] Restarted $PID"
22+
elif [[ $LINE = "exit" ]]; then
23+
# Kill existing process
24+
kill -TERM $PID
25+
exit 0
26+
fi
27+
done

packages/tre/src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,13 @@ export class Miniflare {
265265
};
266266
this.#runtime = new this.#runtimeConstructor(opts);
267267
this.#removeRuntimeExitHook = exitHook(() => void this.#runtime?.dispose());
268-
this.#runtimeEntryURL = new URL(`http://127.0.0.1:${opts.entryPort}`);
268+
269+
const accessibleHost =
270+
this.#runtime.accessibleHostOverride ??
271+
(host === "*" || host === "0.0.0.0" ? "127.0.0.1" : host);
272+
this.#runtimeEntryURL = new URL(
273+
`http://${accessibleHost}:${opts.entryPort}`
274+
);
269275

270276
const config = await this.#assembleConfig();
271277
assert(config !== undefined);
@@ -604,7 +610,7 @@ export class Miniflare {
604610
} finally {
605611
// Cleanup as much as possible even if `#init()` threw
606612
this.#removeRuntimeExitHook?.();
607-
this.#runtime?.dispose();
613+
await this.#runtime?.dispose();
608614
await this.#stopLoopbackServer();
609615
}
610616
}

packages/tre/src/runtime/index.ts

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import crypto from "crypto";
33
import fs from "fs";
44
import os from "os";
55
import path from "path";
6+
import rl from "readline";
7+
import { pathToFileURL } from "url";
68
import { red } from "kleur/colors";
79
import workerdPath, {
810
compatibilityDate as supportedCompatibilityDate,
@@ -21,6 +23,8 @@ export interface RuntimeOptions {
2123
export abstract class Runtime {
2224
constructor(protected readonly opts: RuntimeOptions) {}
2325

26+
accessibleHostOverride?: string;
27+
2428
abstract updateConfig(configBuffer: Buffer): Awaitable<void>;
2529
abstract get exitPromise(): Promise<void> | undefined;
2630
abstract dispose(): Awaitable<void>;
@@ -59,24 +63,17 @@ function waitForExit(process: childProcess.ChildProcess): Promise<void> {
5963
});
6064
}
6165

62-
function trimTrailingNewline(buffer: Buffer) {
63-
let string = buffer.toString();
64-
if (string.endsWith("\n")) string = string.substring(0, string.length - 1);
65-
return string;
66-
}
6766
function pipeOutput(runtime: childProcess.ChildProcessWithoutNullStreams) {
6867
// TODO: may want to proxy these and prettify ✨
6968
// We can't just pipe() to `process.stdout/stderr` here, as Ink (used by
7069
// wrangler), only patches the `console.*` methods:
7170
// https://github.com/vadimdemedes/ink/blob/5d24ed8ada593a6c36ea5416f452158461e33ba5/readme.md#patchconsole
7271
// Writing directly to `process.stdout/stderr` would result in graphical
7372
// glitches.
74-
runtime.stdout.on("data", (data) => {
75-
console.log(trimTrailingNewline(data));
76-
});
77-
runtime.stderr.on("data", (data) => {
78-
console.error(red(trimTrailingNewline(data)));
79-
});
73+
const stdout = rl.createInterface(runtime.stdout);
74+
const stderr = rl.createInterface(runtime.stderr);
75+
stdout.on("line", (data) => console.log(data));
76+
stderr.on("line", (data) => console.error(red(data)));
8077
// runtime.stdout.pipe(process.stdout);
8178
// runtime.stderr.pipe(process.stderr);
8279
}
@@ -143,25 +140,115 @@ class NativeRuntime extends Runtime {
143140
}
144141
}
145142

146-
class WSLRuntime extends NativeRuntime {
143+
// `__dirname` relative to bundled output `dist/src/index.js`
144+
const LIB_PATH = path.resolve(__dirname, "..", "..", "lib");
145+
const WSL_RESTART_PATH = path.join(LIB_PATH, "wsl-restart.sh");
146+
const WSL_EXE_PATH = "C:\\Windows\\System32\\wsl.exe";
147+
148+
class WSLRuntime extends Runtime {
147149
static isSupported() {
148-
return process.platform === "win32"; // TODO: && parse output from `wsl --list --verbose`, may need to check distro?;
150+
if (process.platform !== "win32") return false;
151+
// Make sure we have a WSL distribution installed.
152+
const stdout = childProcess.execSync(`${WSL_EXE_PATH} --list --verbose`, {
153+
encoding: "utf16le",
154+
});
155+
// Example successful result:
156+
// ```
157+
// NAME STATE VERSION
158+
// * Ubuntu-22.04 Running 2
159+
// ```
160+
return (
161+
stdout.includes("NAME") &&
162+
stdout.includes("STATE") &&
163+
stdout.includes("*")
164+
);
149165
}
166+
150167
static supportSuggestion =
151168
"Install the Windows Subsystem for Linux (https://aka.ms/wsl), " +
152169
"then run as you are at the moment";
153170
static description = "using WSL ✨";
154171

155-
getCommand(): string[] {
156-
const command = super.getCommand();
157-
command.unshift("wsl"); // TODO: may need to select distro?
158-
// TODO: may need to convert runtime path to /mnt/c/...
159-
return command;
172+
private static pathToWSL(filePath: string): string {
173+
// "C:\..." ---> "file:///C:/..." ---> "/C:/..."
174+
const { pathname } = pathToFileURL(filePath);
175+
// "/C:/..." ---> "/mnt/c/..."
176+
return pathname.replace(
177+
/^\/([A-Z]):\//i,
178+
(_match, letter) => `/mnt/${letter.toLowerCase()}/`
179+
);
180+
}
181+
182+
#configPath = path.join(
183+
os.tmpdir(),
184+
`miniflare-config-${crypto.randomBytes(16).toString("hex")}.bin`
185+
);
186+
187+
// WSL's localhost forwarding only seems to work when using `localhost` as
188+
// the host.
189+
// https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configuration-setting-for-wslconfig
190+
accessibleHostOverride = "localhost";
191+
192+
#process?: childProcess.ChildProcess;
193+
#processExitPromise?: Promise<void>;
194+
195+
#sendCommand(command: string): void {
196+
this.#process?.stdin?.write(`${command}\n`);
197+
}
198+
199+
async updateConfig(configBuffer: Buffer) {
200+
// 1. Write config to file (this is much easier than trying to buffer STDIN
201+
// in the restart script)
202+
fs.writeFileSync(this.#configPath, configBuffer);
203+
204+
// 2. If process running, send "restart" command to restart runtime with
205+
// new config (see `lib/wsl-restart.sh`)
206+
if (this.#process) {
207+
return this.#sendCommand("restart");
208+
}
209+
210+
// 3. Otherwise, start new process
211+
const runtimeProcess = childProcess.spawn(
212+
WSL_EXE_PATH,
213+
[
214+
"--exec",
215+
WSLRuntime.pathToWSL(WSL_RESTART_PATH),
216+
WSLRuntime.pathToWSL(workerdPath),
217+
...this.getCommonArgs(),
218+
// `*:<port>` is the only address that seems to work with WSL's
219+
// localhost forwarding. `localhost:<port>`/`127.0.0.1:<port>` don't.
220+
// https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configuration-setting-for-wslconfig
221+
`--socket-addr=${SOCKET_ENTRY}=*:${this.opts.entryPort}`,
222+
`--external-addr=${SERVICE_LOOPBACK}=127.0.0.1:${this.opts.loopbackPort}`,
223+
WSLRuntime.pathToWSL(this.#configPath),
224+
],
225+
{ stdio: "pipe" }
226+
);
227+
this.#process = runtimeProcess;
228+
this.#processExitPromise = waitForExit(runtimeProcess);
229+
pipeOutput(runtimeProcess);
230+
}
231+
232+
get exitPromise(): Promise<void> | undefined {
233+
return this.#processExitPromise;
234+
}
235+
236+
dispose(): Awaitable<void> {
237+
this.#sendCommand("exit");
238+
// We probably don't need to kill here, as the "exit" should be enough to
239+
// terminate the restart script. Doesn't hurt though.
240+
this.#process?.kill();
241+
try {
242+
fs.unlinkSync(this.#configPath);
243+
} catch (e: any) {
244+
// Ignore not found errors if we called dispose() without updateConfig()
245+
if (e.code !== "ENOENT") throw e;
246+
}
247+
return this.#processExitPromise;
160248
}
161249
}
162250

163-
// `__dirname` relative to bundled output `dist/src/index.js`
164-
const RESTART_PATH = path.resolve(__dirname, "..", "..", "lib", "restart.sh");
251+
const DOCKER_RESTART_PATH = path.join(LIB_PATH, "docker-restart.sh");
165252

166253
class DockerRuntime extends Runtime {
167254
static isSupported() {
@@ -187,7 +274,7 @@ class DockerRuntime extends Runtime {
187274
fs.writeFileSync(this.#configPath, configBuffer);
188275

189276
// 2. If process running, send SIGUSR1 to restart runtime with new config
190-
// (see `lib/restart.sh`)
277+
// (see `lib/docker-restart.sh`)
191278
if (this.#process) {
192279
this.#process.kill("SIGUSR1");
193280
return;
@@ -201,7 +288,7 @@ class DockerRuntime extends Runtime {
201288
"--platform=linux/amd64",
202289
"--interactive",
203290
"--rm",
204-
`--volume=${RESTART_PATH}:/restart.sh`,
291+
`--volume=${DOCKER_RESTART_PATH}:/restart.sh`,
205292
`--volume=${workerdPath}:/runtime`,
206293
`--volume=${this.#configPath}:/miniflare-config.bin`,
207294
`--publish=${this.opts.entryHost}:${this.opts.entryPort}:8787`,
@@ -258,9 +345,9 @@ export function getSupportedRuntime(): RuntimeConstructor {
258345
);
259346
throw new MiniflareCoreError(
260347
"ERR_RUNTIME_UNSUPPORTED",
261-
`The 🦄 Cloudflare Workers Runtime 🦄 does not support your system (${
262-
process.platform
263-
} ${process.arch}). Either:\n${suggestions.join("\n")}\n`
348+
`workerd does not support your system (${process.platform} ${
349+
process.arch
350+
}). Either:\n${suggestions.join("\n")}\n`
264351
);
265352
}
266353

0 commit comments

Comments
 (0)