@@ -3,6 +3,8 @@ import crypto from "crypto";
33import fs from "fs" ;
44import os from "os" ;
55import path from "path" ;
6+ import rl from "readline" ;
7+ import { pathToFileURL } from "url" ;
68import { red } from "kleur/colors" ;
79import workerdPath , {
810 compatibilityDate as supportedCompatibilityDate ,
@@ -21,6 +23,8 @@ export interface RuntimeOptions {
2123export 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- }
6766function 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
166253class 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