11import childProcess from "child_process" ;
2- import { Awaitable , MiniflareError } from "../helpers" ;
2+ import crypto from "crypto" ;
3+ import fs from "fs" ;
4+ import os from "os" ;
5+ import path from "path" ;
6+ import { Awaitable , MiniflareCoreError } from "../helpers" ;
37import { SERVICE_LOOPBACK , SOCKET_ENTRY } from "../plugins" ;
48
5- export interface Runtime {
6- updateConfig ( configBuffer : Buffer ) : Awaitable < void > ;
7- dispose ( ) : Awaitable < void > ;
9+ export abstract class Runtime {
10+ constructor (
11+ protected readonly runtimeBinaryPath : string ,
12+ protected readonly entryPort : number ,
13+ protected readonly loopbackPort : number
14+ ) { }
15+
16+ abstract updateConfig ( configBuffer : Buffer ) : Awaitable < void > ;
17+ abstract dispose ( ) : Awaitable < void > ;
818}
919
1020export interface RuntimeConstructor {
@@ -21,14 +31,16 @@ export interface RuntimeConstructor {
2131}
2232
2333const COMMON_RUNTIME_ARGS = [ "serve" , "--binary" , "--verbose" ] ;
34+ // `__dirname` relative to bundled output `dist/src/index.js`
35+ const RESTART_PATH = path . resolve ( __dirname , ".." , ".." , "lib" , "restart.sh" ) ;
2436
25- function waitForExit ( process : childProcess . ChildProcess ) : Promise < number > {
37+ function waitForExit ( process : childProcess . ChildProcess ) : Promise < void > {
2638 return new Promise ( ( resolve ) => {
27- process . once ( "exit" , ( code ) => resolve ( code ?? - 1 ) ) ;
39+ process . once ( "exit" , ( ) => resolve ( ) ) ;
2840 } ) ;
2941}
3042
31- class NativeRuntime implements Runtime {
43+ class NativeRuntime extends Runtime {
3244 static isSupported ( ) {
3345 return process . platform === "linux" ; // TODO: and "darwin"?
3446 }
@@ -40,13 +52,14 @@ class NativeRuntime implements Runtime {
4052 readonly #args: string [ ] ;
4153
4254 #process?: childProcess . ChildProcess ;
43- #processExitPromise?: Promise < number > ;
55+ #processExitPromise?: Promise < void > ;
4456
4557 constructor (
46- protected readonly runtimeBinaryPath : string ,
47- protected readonly entryPort : number ,
48- protected readonly loopbackPort : number
58+ runtimeBinaryPath : string ,
59+ entryPort : number ,
60+ loopbackPort : number
4961 ) {
62+ super ( runtimeBinaryPath , entryPort , loopbackPort ) ;
5063 const [ command , ...args ] = this . getCommand ( ) ;
5164 this . #command = command ;
5265 this . #args = args ;
@@ -71,7 +84,7 @@ class NativeRuntime implements Runtime {
7184 // TODO: what happens if runtime crashes?
7285
7386 // 2. Start new process
74- const runtimeProcess = await childProcess . spawn ( this . #command, this . #args, {
87+ const runtimeProcess = childProcess . spawn ( this . #command, this . #args, {
7588 stdio : "pipe" ,
7689 shell : true ,
7790 } ) ;
@@ -86,11 +99,12 @@ class NativeRuntime implements Runtime {
8699
87100 // 3. Write config
88101 runtimeProcess . stdin . write ( configBuffer ) ;
102+ runtimeProcess . stdin . end ( ) ;
89103 }
90104
91- async dispose ( ) {
105+ dispose ( ) : Awaitable < void > {
92106 this . #process?. kill ( ) ;
93- await this . #processExitPromise;
107+ return this . #processExitPromise;
94108 }
95109}
96110
@@ -112,7 +126,7 @@ class WSLRuntime extends NativeRuntime {
112126 }
113127}
114128
115- class DockerRuntime extends NativeRuntime {
129+ class DockerRuntime extends Runtime {
116130 static isSupported ( ) {
117131 const result = childProcess . spawnSync ( "docker" , [ "--version" ] ) ; // TODO: check daemon running too?
118132 return result . error === undefined ;
@@ -123,23 +137,68 @@ class DockerRuntime extends NativeRuntime {
123137 static description = "using Docker 🐳" ;
124138 static distribution = `linux-${ process . arch } ` ;
125139
126- getCommand ( ) : string [ ] {
127- // TODO: consider reusing container, but just restarting process within
128- return [
140+ #configPath = path . join (
141+ os . tmpdir ( ) ,
142+ `miniflare-config-${ crypto . randomBytes ( 16 ) . toString ( "hex" ) } .bin`
143+ ) ;
144+
145+ #process?: childProcess . ChildProcess ;
146+ #processExitPromise?: Promise < void > ;
147+
148+ async updateConfig ( configBuffer : Buffer ) {
149+ // 1. Write config to file (this is much easier than trying to buffer STDIN
150+ // in the restart script)
151+ fs . writeFileSync ( this . #configPath, configBuffer ) ;
152+
153+ // 2. If process running, send SIGUSR1 to restart runtime with new config
154+ // (see `lib/restart.sh`)
155+ if ( this . #process) {
156+ this . #process. kill ( "SIGUSR1" ) ;
157+ return ;
158+ }
159+
160+ // 3. Otherwise, start new process
161+ const runtimeProcess = childProcess . spawn (
129162 "docker" ,
130- "run" ,
131- "--platform=linux/amd64" ,
132- "--interactive" ,
133- "--rm" ,
134- `--volume=${ this . runtimeBinaryPath } :/runtime` ,
135- `--publish=127.0.0.1:${ this . entryPort } :8787` ,
136- "debian:bullseye-slim" ,
137- "/runtime" ,
138- ...COMMON_RUNTIME_ARGS ,
139- `--socket-addr=${ SOCKET_ENTRY } =*:8787` ,
140- `--external-addr=${ SERVICE_LOOPBACK } =host.docker.internal:${ this . loopbackPort } ` ,
141- "-" ,
142- ] ;
163+ [
164+ "run" ,
165+ "--platform=linux/amd64" ,
166+ "--interactive" ,
167+ "--rm" ,
168+ `--volume=${ RESTART_PATH } :/restart.sh` ,
169+ `--volume=${ this . runtimeBinaryPath } :/runtime` ,
170+ `--volume=${ this . #configPath} :/miniflare-config.bin` ,
171+ `--publish=127.0.0.1:${ this . entryPort } :8787` ,
172+ "debian:bullseye-slim" ,
173+ "/restart.sh" ,
174+ "/runtime" ,
175+ ...COMMON_RUNTIME_ARGS ,
176+ `--socket-addr=${ SOCKET_ENTRY } =*:8787` ,
177+ `--external-addr=${ SERVICE_LOOPBACK } =host.docker.internal:${ this . loopbackPort } ` ,
178+ "/miniflare-config.bin" ,
179+ ] ,
180+ {
181+ stdio : "pipe" ,
182+ shell : true ,
183+ }
184+ ) ;
185+ this . #process = runtimeProcess ;
186+ this . #processExitPromise = waitForExit ( runtimeProcess ) ;
187+
188+ // TODO: may want to proxy these and prettify ✨
189+ runtimeProcess . stdout . pipe ( process . stdout ) ;
190+ runtimeProcess . stderr . pipe ( process . stderr ) ;
191+ }
192+
193+ dispose ( ) : Awaitable < void > {
194+ this . #process?. kill ( ) ;
195+ try {
196+ fs . unlinkSync ( this . #configPath) ;
197+ } catch ( e : any ) {
198+ // Ignore not found errors if we called dispose() without updateConfig()
199+ if ( e . code !== "ENOENT" ) throw e ;
200+ }
201+ return this . #processExitPromise;
143202 }
144203}
145204
@@ -160,7 +219,7 @@ export function getSupportedRuntime(): RuntimeConstructor {
160219 const suggestions = RUNTIMES . map (
161220 ( { supportSuggestion } ) => `- ${ supportSuggestion } `
162221 ) ;
163- throw new MiniflareError (
222+ throw new MiniflareCoreError (
164223 "ERR_RUNTIME_UNSUPPORTED" ,
165224 `The 🦄 Cloudflare Workers Runtime 🦄 does not support your system (${
166225 process . platform
0 commit comments