diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87aa60130..39f903ab7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest] # `windows-latest` not yet supported + os: [ubuntu-latest, windows-latest] node: [16.13.0, latest] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ diff --git a/package-lock.json b/package-lock.json index aee5e5dbd..d97324fbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@miniflare/root", "version": "3.0.0-next.12", + "hasInstallScript": true, "license": "MIT", "workspaces": [ "packages/*" @@ -82,9 +83,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20230221.0.tgz", - "integrity": "sha512-8yBpFqSO7Rw/Z/c/zmlO5KoGZDJ5ar8KH8xXyPMxUN6hqpKv9tg7tTJCi6rjRhSneonprKBtnnSShCUNeY4sMw==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20230404.0.tgz", + "integrity": "sha512-W36JJLz3XLktBv+Huw37eZhiHvXp8XZ+1LsbUqU2O5Y0OXUzVhxhtdPSHgxvjfwo83zxnWuSS3uGKl6ex8Xm/Q==", "cpu": [ "x64" ], @@ -97,9 +98,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20230221.0.tgz", - "integrity": "sha512-+Q/SFBqvMZOkC3McxaQkZENSFeQIwodIzMcy8uoa8nOLLBqCg2POmg9drTZBH51gzxCsgTeXi5dxxJl3yZdLkg==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20230404.0.tgz", + "integrity": "sha512-cEpLgMwNmUpXVPjTJEd+WXgzSt9l4cTJpaNyEouAHrH99cStt21iIgkVctnef9bpPqzLoIZf2LUaPKu2H85/Ig==", "cpu": [ "arm64" ], @@ -112,25 +113,24 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20230221.0.tgz", - "integrity": "sha512-9kR/PFdcudzyOwXpnC2LAXXwfsbby2YIPVzRQESGqXjWxls34dh4BlOgVTzccKPIDCg3luGgOj2x6fKbp8jbNw==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20230404.0.tgz", + "integrity": "sha512-XMXUuZKTfWW60EKLYy0DJJSEKF8/q1EupYa2nldIp2i6gprhndXxOe3WR4inWB+XAMcppUXO0VgwAV6tTvgOvg==", "cpu": [ "x64" ], "optional": true, "os": [ - "linux", - "win32" + "linux" ], "engines": { "node": ">=16" } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20230221.0.tgz", - "integrity": "sha512-HiGpjTdxFC+ZzLd7M3adgegqvljePuctdP7GDmGLBul3N3w5kfyy1j+vDw5Xq/FEvqLc7jhdS4S7/8eMfU2P+w==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20230404.0.tgz", + "integrity": "sha512-4+mnzlnlYe66LusX0HeS5TNV4FN64QSMD/WuahbFPpxg41t/iBD4T8KrRYvz0PjIhNzt8wqxymHbL1Coo+La5Q==", "cpu": [ "arm64" ], @@ -142,6 +142,21 @@ "node": ">=16" } }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20230404.0.tgz", + "integrity": "sha512-LRuLa6dqYlFGSfeqYV+f0J2llUTQ5nuWDrp77pqSHgJlqFFEX2qlZ0wJIAxulvK5Vz/O05WvmpdV9HJwrlj3VQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@cloudflare/workers-types": { "version": "4.20221111.1", "dev": true, @@ -4767,9 +4782,9 @@ } }, "node_modules/workerd": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20230221.0.tgz", - "integrity": "sha512-YMwTskgRys9xn/zZXAOd7vTjbnr/obh2rU7blIO+JY+LY5uZuH2AGi0chIXo7lGfLNlQ8HZqtFfurRzBhKbSHg==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20230404.0.tgz", + "integrity": "sha512-tTiWZkJXetNmahmVZL1AcFH/U1cp9MMYRcQNBD1RjTSGh/v8VlKlEKTp4Ss6EdItvPNKQqsE7s/6y/TrblTI7g==", "hasInstallScript": true, "bin": { "workerd": "bin/workerd" @@ -4778,10 +4793,11 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20230221.0", - "@cloudflare/workerd-darwin-arm64": "1.20230221.0", - "@cloudflare/workerd-linux-64": "1.20230221.0", - "@cloudflare/workerd-linux-arm64": "1.20230221.0" + "@cloudflare/workerd-darwin-64": "1.20230404.0", + "@cloudflare/workerd-darwin-arm64": "1.20230404.0", + "@cloudflare/workerd-linux-64": "1.20230404.0", + "@cloudflare/workerd-linux-arm64": "1.20230404.0", + "@cloudflare/workerd-windows-64": "1.20230404.0" } }, "node_modules/wrap-ansi": { @@ -5048,7 +5064,7 @@ "source-map-support": "0.5.21", "stoppable": "^1.1.0", "undici": "^5.13.0", - "workerd": "^1.20230221.0", + "workerd": "^1.20230404.0", "ws": "^8.11.0", "youch": "^3.2.2", "zod": "^3.20.6" @@ -5093,27 +5109,33 @@ } }, "@cloudflare/workerd-darwin-64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20230221.0.tgz", - "integrity": "sha512-8yBpFqSO7Rw/Z/c/zmlO5KoGZDJ5ar8KH8xXyPMxUN6hqpKv9tg7tTJCi6rjRhSneonprKBtnnSShCUNeY4sMw==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20230404.0.tgz", + "integrity": "sha512-W36JJLz3XLktBv+Huw37eZhiHvXp8XZ+1LsbUqU2O5Y0OXUzVhxhtdPSHgxvjfwo83zxnWuSS3uGKl6ex8Xm/Q==", "optional": true }, "@cloudflare/workerd-darwin-arm64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20230221.0.tgz", - "integrity": "sha512-+Q/SFBqvMZOkC3McxaQkZENSFeQIwodIzMcy8uoa8nOLLBqCg2POmg9drTZBH51gzxCsgTeXi5dxxJl3yZdLkg==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20230404.0.tgz", + "integrity": "sha512-cEpLgMwNmUpXVPjTJEd+WXgzSt9l4cTJpaNyEouAHrH99cStt21iIgkVctnef9bpPqzLoIZf2LUaPKu2H85/Ig==", "optional": true }, "@cloudflare/workerd-linux-64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20230221.0.tgz", - "integrity": "sha512-9kR/PFdcudzyOwXpnC2LAXXwfsbby2YIPVzRQESGqXjWxls34dh4BlOgVTzccKPIDCg3luGgOj2x6fKbp8jbNw==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20230404.0.tgz", + "integrity": "sha512-XMXUuZKTfWW60EKLYy0DJJSEKF8/q1EupYa2nldIp2i6gprhndXxOe3WR4inWB+XAMcppUXO0VgwAV6tTvgOvg==", "optional": true }, "@cloudflare/workerd-linux-arm64": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20230221.0.tgz", - "integrity": "sha512-HiGpjTdxFC+ZzLd7M3adgegqvljePuctdP7GDmGLBul3N3w5kfyy1j+vDw5Xq/FEvqLc7jhdS4S7/8eMfU2P+w==", + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20230404.0.tgz", + "integrity": "sha512-4+mnzlnlYe66LusX0HeS5TNV4FN64QSMD/WuahbFPpxg41t/iBD4T8KrRYvz0PjIhNzt8wqxymHbL1Coo+La5Q==", + "optional": true + }, + "@cloudflare/workerd-windows-64": { + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20230404.0.tgz", + "integrity": "sha512-LRuLa6dqYlFGSfeqYV+f0J2llUTQ5nuWDrp77pqSHgJlqFFEX2qlZ0wJIAxulvK5Vz/O05WvmpdV9HJwrlj3VQ==", "optional": true }, "@cloudflare/workers-types": { @@ -5227,7 +5249,7 @@ "source-map-support": "0.5.21", "stoppable": "^1.1.0", "undici": "^5.13.0", - "workerd": "^1.20230221.0", + "workerd": "^1.20230404.0", "ws": "^8.11.0", "youch": "^3.2.2", "zod": "^3.20.6" @@ -7915,14 +7937,15 @@ "dev": true }, "workerd": { - "version": "1.20230221.0", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20230221.0.tgz", - "integrity": "sha512-YMwTskgRys9xn/zZXAOd7vTjbnr/obh2rU7blIO+JY+LY5uZuH2AGi0chIXo7lGfLNlQ8HZqtFfurRzBhKbSHg==", - "requires": { - "@cloudflare/workerd-darwin-64": "1.20230221.0", - "@cloudflare/workerd-darwin-arm64": "1.20230221.0", - "@cloudflare/workerd-linux-64": "1.20230221.0", - "@cloudflare/workerd-linux-arm64": "1.20230221.0" + "version": "1.20230404.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20230404.0.tgz", + "integrity": "sha512-tTiWZkJXetNmahmVZL1AcFH/U1cp9MMYRcQNBD1RjTSGh/v8VlKlEKTp4Ss6EdItvPNKQqsE7s/6y/TrblTI7g==", + "requires": { + "@cloudflare/workerd-darwin-64": "1.20230404.0", + "@cloudflare/workerd-darwin-arm64": "1.20230404.0", + "@cloudflare/workerd-linux-64": "1.20230404.0", + "@cloudflare/workerd-linux-arm64": "1.20230404.0", + "@cloudflare/workerd-windows-64": "1.20230404.0" } }, "wrap-ansi": { diff --git a/package.json b/package.json index f8d661cc4..60054e311 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,13 @@ "dev": "node scripts/build.mjs watch", "helpers:scaffold": "node scripts/scaffold.mjs", "helpers:version": "node scripts/version.mjs", - "lint": "eslint 'packages/*/{src,test}/**/*.ts' 'scripts/**/*.{js,mjs}' 'types/**/*.ts'", + "lint": "eslint \"packages/*/{src,test}/**/*.ts\" \"scripts/**/*.{js,mjs}\" \"types/**/*.ts\"", "lint:fix": "npm run lint -- --fix", "prepublishOnly": "npm run lint && npm run clean && npm run build && npm run types:bundle && npm run test", "test": "npm run build && ava && rimraf ./.tmp", "types:build": "tsc", "___$comment:types:bundle": "api-extractor doesn't know to load index.ts instead of index.d.ts when resolving imported types", - "types:bundle": "cp node_modules/@cloudflare/workers-types/experimental/index.ts node_modules/@cloudflare/workers-types/experimental/index.d.ts && npm run types:build && node scripts/types.mjs" + "types:bundle": "node scripts/copy.mjs node_modules/@cloudflare/workers-types/experimental/index.ts node_modules/@cloudflare/workers-types/experimental/index.d.ts && npm run types:build && node scripts/types.mjs" }, "devDependencies": { "@ava/get-port": "^2.0.0", diff --git a/packages/tre/package.json b/packages/tre/package.json index 5cf3386c7..a36b94046 100644 --- a/packages/tre/package.json +++ b/packages/tre/package.json @@ -39,7 +39,7 @@ "source-map-support": "0.5.21", "stoppable": "^1.1.0", "undici": "^5.13.0", - "workerd": "^1.20230221.0", + "workerd": "^1.20230404.0", "ws": "^8.11.0", "youch": "^3.2.2", "zod": "^3.20.6" diff --git a/packages/tre/src/index.ts b/packages/tre/src/index.ts index b74cebc43..a31709f41 100644 --- a/packages/tre/src/index.ts +++ b/packages/tre/src/index.ts @@ -54,13 +54,11 @@ import { import { Config, Runtime, - RuntimeConstructor, RuntimeOptions, Service, Socket, Worker_Binding, Worker_Module, - getSupportedRuntime, serializeConfig, } from "./runtime"; import { @@ -232,7 +230,6 @@ export class Miniflare { #log: Log; readonly #clock: Clock; - readonly #runtimeConstructor: RuntimeConstructor; #runtime?: Runtime; #removeRuntimeExitHook?: () => void; #runtimeEntryURL?: URL; @@ -286,12 +283,6 @@ export class Miniflare { this.#clock = this.#sharedOpts.core.clock ?? defaultClock; this.#initPlugins(); - // Get supported shell for executing runtime binary - // TODO: allow this to be configured if necessary - this.#runtimeConstructor = getSupportedRuntime(); - const desc = this.#runtimeConstructor.description; - this.#log.debug(`Running workerd ${desc}...`); - this.#liveReloadServer = new WebSocketServer({ noServer: true }); this.#webSocketServer = new WebSocketServer({ noServer: true, @@ -386,12 +377,11 @@ export class Miniflare { inspectorPort: this.#sharedOpts.core.inspectorPort, verbose: this.#sharedOpts.core.verbose, }; - this.#runtime = new this.#runtimeConstructor(opts); + this.#runtime = new Runtime(opts); this.#removeRuntimeExitHook = exitHook(() => void this.#runtime?.dispose()); const accessibleHost = - this.#runtime.accessibleHostOverride ?? - (host === "*" || host === "0.0.0.0" ? "127.0.0.1" : host); + host === "*" || host === "0.0.0.0" ? "127.0.0.1" : host; this.#runtimeEntryURL = new URL(`http://${accessibleHost}:${entryPort}`); const config = await this.#assembleConfig(); diff --git a/packages/tre/src/plugins/shared/gateway.ts b/packages/tre/src/plugins/shared/gateway.ts index 5696e9548..095a06131 100644 --- a/packages/tre/src/plugins/shared/gateway.ts +++ b/packages/tre/src/plugins/shared/gateway.ts @@ -50,8 +50,9 @@ export const DEFAULT_PERSIST_ROOT = ".mf"; export const PARAM_FILE_UNSANITISE = "unsanitise"; export function maybeParseURL(url: Persistence): URL | undefined { + if (typeof url !== "string" || path.isAbsolute(url)) return; try { - if (typeof url === "string") return new URL(url); + return new URL(url); } catch {} } diff --git a/packages/tre/src/runtime/index.ts b/packages/tre/src/runtime/index.ts index c0a9e2e58..2c4160ced 100644 --- a/packages/tre/src/runtime/index.ts +++ b/packages/tre/src/runtime/index.ts @@ -1,61 +1,11 @@ import childProcess from "child_process"; -import crypto from "crypto"; -import fs from "fs"; -import os from "os"; -import path from "path"; import rl from "readline"; -import { pathToFileURL } from "url"; import { red } from "kleur/colors"; import workerdPath, { compatibilityDate as supportedCompatibilityDate, } from "workerd"; import { SERVICE_LOOPBACK, SOCKET_ENTRY } from "../plugins"; -import { Awaitable, MiniflareCoreError } from "../shared"; - -export interface RuntimeOptions { - entryHost: string; - entryPort: number; - loopbackPort: number; - inspectorPort?: number; - verbose?: boolean; -} - -export abstract class Runtime { - constructor(protected readonly opts: RuntimeOptions) {} - - accessibleHostOverride?: string; - - abstract updateConfig(configBuffer: Buffer): Awaitable; - abstract get exitPromise(): Promise | undefined; - abstract dispose(): Awaitable; - - protected getCommonArgs(): string[] { - const args: string[] = [ - "serve", - // Required to use binary capnp config - "--binary", - // Required to use compatibility flags without a default-on date, - // (e.g. "streams_enable_constructors"), see https://github.com/cloudflare/workerd/pull/21 - "--experimental", - ]; - if (this.opts.inspectorPort !== undefined) { - // Required to enable the V8 inspector - args.push(`--inspector-addr=127.0.0.1:${this.opts.inspectorPort}`); - } - if (this.opts.verbose) { - args.push("--verbose"); - } - return args; - } -} - -export interface RuntimeConstructor { - new (opts: RuntimeOptions): Runtime; - - isSupported(): boolean; - supportSuggestion: string; - description: string; -} +import { Awaitable } from "../shared"; function waitForExit(process: childProcess.ChildProcess): Promise { return new Promise((resolve) => { @@ -78,37 +28,44 @@ function pipeOutput(runtime: childProcess.ChildProcessWithoutNullStreams) { // runtime.stderr.pipe(process.stderr); } -class NativeRuntime extends Runtime { - static isSupported() { - return process.platform === "linux" || process.platform === "darwin"; - } - static supportSuggestion = "Run using a Linux or macOS based system"; - static description = "natively ⚡️"; +export interface RuntimeOptions { + entryHost: string; + entryPort: number; + loopbackPort: number; + inspectorPort?: number; + verbose?: boolean; +} +export class Runtime { readonly #command: string; readonly #args: string[]; #process?: childProcess.ChildProcess; #processExitPromise?: Promise; - constructor(opts: RuntimeOptions) { - super(opts); - const [command, ...args] = this.getCommand(); - this.#command = command; - this.#args = args; - } - - getCommand(): string[] { - return [ - workerdPath, - ...this.getCommonArgs(), + constructor(private opts: RuntimeOptions) { + const args: string[] = [ + "serve", + // Required to use binary capnp config + "--binary", + // Required to use compatibility flags without a default-on date, + // (e.g. "streams_enable_constructors"), see https://github.com/cloudflare/workerd/pull/21 + "--experimental", `--socket-addr=${SOCKET_ENTRY}=${this.opts.entryHost}:${this.opts.entryPort}`, `--external-addr=${SERVICE_LOOPBACK}=127.0.0.1:${this.opts.loopbackPort}`, - // TODO: consider adding support for unix sockets? - // `--socket-fd=${SOCKET_ENTRY}=${this.entryPort}`, - // `--external-addr=${SERVICE_LOOPBACK}=${this.loopbackPort}`, + // Read config from stdin "-", ]; + if (this.opts.inspectorPort !== undefined) { + // Required to enable the V8 inspector + args.push(`--inspector-addr=127.0.0.1:${this.opts.inspectorPort}`); + } + if (this.opts.verbose) { + args.push("--verbose"); + } + + this.#command = workerdPath; + this.#args = args; } async updateConfig(configBuffer: Buffer) { @@ -145,216 +102,5 @@ class NativeRuntime extends Runtime { } } -// `__dirname` relative to bundled output `dist/src/index.js` -const LIB_PATH = path.resolve(__dirname, "..", "..", "lib"); -const WSL_RESTART_PATH = path.join(LIB_PATH, "wsl-restart.sh"); -const WSL_EXE_PATH = "C:\\Windows\\System32\\wsl.exe"; - -class WSLRuntime extends Runtime { - static isSupported() { - if (process.platform !== "win32") return false; - // Make sure we have a WSL distribution installed. - const stdout = childProcess.execSync(`${WSL_EXE_PATH} --list --verbose`, { - encoding: "utf16le", - }); - // Example successful result: - // ``` - // NAME STATE VERSION - // * Ubuntu-22.04 Running 2 - // ``` - return ( - stdout.includes("NAME") && - stdout.includes("STATE") && - stdout.includes("*") - ); - } - - static supportSuggestion = - "Install the Windows Subsystem for Linux (https://aka.ms/wsl), " + - "then run as you are at the moment"; - static description = "using WSL ✨"; - - private static pathToWSL(filePath: string): string { - // "C:\..." ---> "file:///C:/..." ---> "/C:/..." - const { pathname } = pathToFileURL(filePath); - // "/C:/..." ---> "/mnt/c/..." - return pathname.replace( - /^\/([A-Z]):\//i, - (_match, letter) => `/mnt/${letter.toLowerCase()}/` - ); - } - - #configPath = path.join( - os.tmpdir(), - `miniflare-config-${crypto.randomBytes(16).toString("hex")}.bin` - ); - - // WSL's localhost forwarding only seems to work when using `localhost` as - // the host. - // https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configuration-setting-for-wslconfig - accessibleHostOverride = "localhost"; - - #process?: childProcess.ChildProcess; - #processExitPromise?: Promise; - - #sendCommand(command: string): void { - this.#process?.stdin?.write(`${command}\n`); - } - - async updateConfig(configBuffer: Buffer) { - // 1. Write config to file (this is much easier than trying to buffer STDIN - // in the restart script) - fs.writeFileSync(this.#configPath, configBuffer); - - // 2. If process running, send "restart" command to restart runtime with - // new config (see `lib/wsl-restart.sh`) - if (this.#process) { - return this.#sendCommand("restart"); - } - - // 3. Otherwise, start new process - const runtimeProcess = childProcess.spawn( - WSL_EXE_PATH, - [ - "--exec", - WSLRuntime.pathToWSL(WSL_RESTART_PATH), - WSLRuntime.pathToWSL(workerdPath), - ...this.getCommonArgs(), - // `*:` is the only address that seems to work with WSL's - // localhost forwarding. `localhost:`/`127.0.0.1:` don't. - // https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configuration-setting-for-wslconfig - `--socket-addr=${SOCKET_ENTRY}=*:${this.opts.entryPort}`, - `--external-addr=${SERVICE_LOOPBACK}=127.0.0.1:${this.opts.loopbackPort}`, - WSLRuntime.pathToWSL(this.#configPath), - ], - { stdio: "pipe" } - ); - this.#process = runtimeProcess; - this.#processExitPromise = waitForExit(runtimeProcess); - pipeOutput(runtimeProcess); - } - - get exitPromise(): Promise | undefined { - return this.#processExitPromise; - } - - dispose(): Awaitable { - this.#sendCommand("exit"); - // We probably don't need to kill here, as the "exit" should be enough to - // terminate the restart script. Doesn't hurt though. - this.#process?.kill(); - try { - fs.unlinkSync(this.#configPath); - } catch (e: any) { - // Ignore not found errors if we called dispose() without updateConfig() - if (e.code !== "ENOENT") throw e; - } - return this.#processExitPromise; - } -} - -const DOCKER_RESTART_PATH = path.join(LIB_PATH, "docker-restart.sh"); - -class DockerRuntime extends Runtime { - static isSupported() { - const result = childProcess.spawnSync("docker", ["--version"]); // TODO: check daemon running too? - return result.error === undefined; - } - static supportSuggestion = - "Install Docker Desktop (https://www.docker.com/products/docker-desktop/), " + - "then run as you are at the moment"; - static description = "using Docker 🐳"; - - #configPath = path.join( - os.tmpdir(), - `miniflare-config-${crypto.randomBytes(16).toString("hex")}.bin` - ); - - #process?: childProcess.ChildProcess; - #processExitPromise?: Promise; - - async updateConfig(configBuffer: Buffer) { - // 1. Write config to file (this is much easier than trying to buffer STDIN - // in the restart script) - fs.writeFileSync(this.#configPath, configBuffer); - - // 2. If process running, send SIGUSR1 to restart runtime with new config - // (see `lib/docker-restart.sh`) - if (this.#process) { - this.#process.kill("SIGUSR1"); - return; - } - - // 3. Otherwise, start new process - const runtimeProcess = childProcess.spawn( - "docker", - [ - "run", - "--platform=linux/amd64", - "--interactive", - "--rm", - `--volume=${DOCKER_RESTART_PATH}:/restart.sh`, - `--volume=${workerdPath}:/runtime`, - `--volume=${this.#configPath}:/miniflare-config.bin`, - `--publish=${this.opts.entryHost}:${this.opts.entryPort}:8787`, - "debian:bullseye-slim", - "/restart.sh", - "/runtime", - ...this.getCommonArgs(), - `--socket-addr=${SOCKET_ENTRY}=*:8787`, - `--external-addr=${SERVICE_LOOPBACK}=host.docker.internal:${this.opts.loopbackPort}`, - "/miniflare-config.bin", - ], - { - stdio: "pipe", - shell: true, - } - ); - this.#process = runtimeProcess; - this.#processExitPromise = waitForExit(runtimeProcess); - pipeOutput(runtimeProcess); - } - - get exitPromise(): Promise | undefined { - return this.#processExitPromise; - } - - dispose(): Awaitable { - this.#process?.kill(); - try { - fs.unlinkSync(this.#configPath); - } catch (e: any) { - // Ignore not found errors if we called dispose() without updateConfig() - if (e.code !== "ENOENT") throw e; - } - return this.#processExitPromise; - } -} - -const RUNTIMES = [NativeRuntime, WSLRuntime, DockerRuntime]; -let supportedRuntime: RuntimeConstructor; -export function getSupportedRuntime(): RuntimeConstructor { - // Return cached result to avoid checking support more than required - if (supportedRuntime !== undefined) return supportedRuntime; - - // Return and cache the best runtime (`RUNTIMES` is sorted by preference) - for (const runtime of RUNTIMES) { - if (runtime.isSupported()) { - return (supportedRuntime = runtime); - } - } - - // Throw with installation suggestions if we couldn't find a supported one - const suggestions = RUNTIMES.map( - ({ supportSuggestion }) => `- ${supportSuggestion}` - ); - throw new MiniflareCoreError( - "ERR_RUNTIME_UNSUPPORTED", - `workerd does not support your system (${process.platform} ${ - process.arch - }). Either:\n${suggestions.join("\n")}\n` - ); -} - export * from "./config"; export { supportedCompatibilityDate }; diff --git a/packages/tre/src/shared/error.ts b/packages/tre/src/shared/error.ts index 756006cd9..32e089511 100644 --- a/packages/tre/src/shared/error.ts +++ b/packages/tre/src/shared/error.ts @@ -13,7 +13,6 @@ export class MiniflareError< } export type MiniflareCoreErrorCode = - | "ERR_RUNTIME_UNSUPPORTED" // System doesn't support Cloudflare Workers runtime | "ERR_RUNTIME_FAILURE" // Runtime failed to start | "ERR_DISPOSED" // Attempted to use Miniflare instance after calling dispose() | "ERR_MODULE_PARSE" // SyntaxError when attempting to parse/locate modules diff --git a/packages/tre/test/plugins/do/index.spec.ts b/packages/tre/test/plugins/do/index.spec.ts index bdbb55d03..0bec41fb1 100644 --- a/packages/tre/test/plugins/do/index.spec.ts +++ b/packages/tre/test/plugins/do/index.spec.ts @@ -1,7 +1,7 @@ import fs from "fs/promises"; import path from "path"; import { Miniflare, MiniflareOptions } from "@miniflare/tre"; -import test from "ava"; +import anyTest from "ava"; import { getPort, useTmp } from "../../test-shared"; const COUNTER_SCRIPT = (responsePrefix = "") => `export class Counter { @@ -24,6 +24,8 @@ export default { }, };`; +const test = anyTest.serial; + test("persists Durable Object data in-memory between options reloads", async (t) => { const opts: MiniflareOptions = { port: await getPort(), @@ -89,9 +91,12 @@ test("persists Durable Object data on file-system", async (t) => { t.is(await res.text(), "2"); // Check removing persisted data then reloaded resets count (note we have to - // reload here as `workerd` keeps a copy of the SQLite database in-memory) + // reload here as `workerd` keeps a copy of the SQLite database in-memory, + // we also need to `dispose()` to avoid `EBUSY` error on Windows) + await mf.dispose(); await fs.rm(path.join(tmp, names[0]), { force: true, recursive: true }); - await mf.setOptions(opts); + + mf = new Miniflare(opts); res = await mf.dispatchFetch("http://localhost"); t.is(await res.text(), "1"); diff --git a/packages/tre/test/plugins/r2/index.spec.ts b/packages/tre/test/plugins/r2/index.spec.ts index 9c75fdfac..c5ab75adc 100644 --- a/packages/tre/test/plugins/r2/index.spec.ts +++ b/packages/tre/test/plugins/r2/index.spec.ts @@ -44,6 +44,8 @@ import { } from "../../test-shared"; import { isWithin } from "../../test-shared/asserts"; +const WITHIN_EPSILON = 10_000; + function hash(value: string, algorithm = "md5") { return crypto.createHash(algorithm).update(value).digest("hex"); } @@ -482,7 +484,7 @@ test("head: returns metadata for existing keys", async (t) => { }); t.deepEqual(object.customMetadata, { key: "value" }); t.deepEqual(object.range, { offset: 0, length: 5 }); - isWithin(t, 3000, object.uploaded.getTime(), start); + isWithin(t, WITHIN_EPSILON, object.uploaded.getTime(), start); }); test(validatesKeyMacro, { method: "head", f: (r2, key) => r2.head(key) }); @@ -524,7 +526,7 @@ test("get: returns metadata and body for existing keys", async (t) => { }); t.deepEqual(body.customMetadata, { key: "value" }); t.deepEqual(body.range, { offset: 0, length: 5 }); - isWithin(t, 3000, body.uploaded.getTime(), start); + isWithin(t, WITHIN_EPSILON, body.uploaded.getTime(), start); }); test(validatesKeyMacro, { method: "get", f: (r2, key) => r2.get(key) }); test("get: range using object", async (t) => { @@ -689,7 +691,7 @@ test("put: returns metadata for created object", async (t) => { }); t.deepEqual(object.customMetadata, { key: "value" }); t.is(object.range, undefined); - isWithin(t, 3000, object.uploaded.getTime(), start); + isWithin(t, WITHIN_EPSILON, object.uploaded.getTime(), start); }); test("put: overrides existing keys", async (t) => { const { r2 } = t.context; @@ -1002,7 +1004,7 @@ test("list: returns metadata with objects", async (t) => { t.deepEqual(object.httpMetadata, {}); t.deepEqual(object.customMetadata, {}); t.is(object.range, undefined); - isWithin(t, 3000, object.uploaded.getTime(), start); + isWithin(t, WITHIN_EPSILON, object.uploaded.getTime(), start); }); test("list: paginates with variable limit", async (t) => { const { r2, ns } = t.context; diff --git a/packages/tre/test/shared/matcher.spec.ts b/packages/tre/test/shared/matcher.spec.ts index 5953c1789..408115b81 100644 --- a/packages/tre/test/shared/matcher.spec.ts +++ b/packages/tre/test/shared/matcher.spec.ts @@ -1,4 +1,5 @@ import path from "path"; +import { pathToFileURL } from "url"; import { globsToRegExps, testRegExps } from "@miniflare/tre"; import test from "ava"; @@ -25,5 +26,10 @@ test("globsToRegExps/testRegExps: matches glob patterns", (t) => { // Check absolute paths (`ModuleLinker` will `path.resolve` to absolute paths) // (see https://github.com/cloudflare/miniflare/issues/244) t.true(testRegExps(matcherRegExps, "/one/two/three.txt")); - t.true(testRegExps(matcherRegExps, path.join(process.cwd(), "src/index.js"))); + t.true( + testRegExps( + matcherRegExps, + pathToFileURL(path.join(process.cwd(), "src/index.js")).href + ) + ); }); diff --git a/scripts/copy.mjs b/scripts/copy.mjs new file mode 100644 index 000000000..40fb84dab --- /dev/null +++ b/scripts/copy.mjs @@ -0,0 +1,7 @@ +import assert from "assert"; +import fs from "fs"; + +const argv = process.argv.slice(2); +assert.strictEqual(argv.length, 2); + +fs.createReadStream(argv[0]).pipe(fs.createWriteStream(argv[1]));