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

Commit 4d1c0e7

Browse files
committed
Add native, WSL and Docker runtimes
1 parent cab964a commit 4d1c0e7

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# TODO: remove prior to release
2+
sserve-conf*
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Data, List, Message, Struct } from "capnp-ts";
2+
import { Config } from "./sserve-conf";
3+
import { Config as CapnpConfig } from "./sserve-conf.capnp.js";
4+
5+
function capitalize<S extends string>(str: S): Capitalize<S> {
6+
return (
7+
str.length > 0 ? str[0].toUpperCase() + str.substring(1) : str
8+
) as Capitalize<S>;
9+
}
10+
11+
// TODO(important): this will fail if someone sets `{ script: undefined }` or
12+
// something manually, where we're expecting an optional string, need a better
13+
// solution
14+
function encodeCapnpStruct(obj: any, struct: Struct, padding = "") {
15+
const anyStruct = struct as any;
16+
for (const [key, value] of Object.entries(obj)) {
17+
const capitalized = capitalize(key);
18+
if (value instanceof Uint8Array) {
19+
const newData: Data = anyStruct[`init${capitalized}`](value.byteLength);
20+
newData.copyBuffer(value);
21+
} else if (Array.isArray(value)) {
22+
const newList: List<any> = anyStruct[`init${capitalized}`](value.length);
23+
for (let i = 0; i < value.length; i++) {
24+
if (typeof value[i] === "object") {
25+
encodeCapnpStruct(value[i], newList.get(i), padding + " ");
26+
} else {
27+
newList.set(i, value[i]);
28+
}
29+
}
30+
} else if (typeof value === "object") {
31+
const newStruct: Struct = anyStruct[`init${capitalized}`]();
32+
encodeCapnpStruct(value, newStruct, padding + " ");
33+
} else {
34+
// TODO: could we catch here if value is actually undefined, but meant to
35+
// be a different type
36+
anyStruct[`set${capitalized}`](value);
37+
}
38+
}
39+
}
40+
41+
export function serializeConfig(config: Config): Buffer {
42+
const message = new Message();
43+
const struct = message.initRoot(CapnpConfig);
44+
encodeCapnpStruct(config, struct);
45+
return Buffer.from(message.toArrayBuffer());
46+
}
47+
48+
export * from "./sserve-conf";

packages/tre/src/runtime/index.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import childProcess from "child_process";
2+
import { Awaitable, MiniflareError } from "../helpers";
3+
import { SERVICE_LOOPBACK, SOCKET_ENTRY } from "../plugins";
4+
5+
export interface Runtime {
6+
updateConfig(configBuffer: Buffer): Awaitable<void>;
7+
dispose(): Awaitable<void>;
8+
}
9+
10+
export interface RuntimeConstructor {
11+
new (
12+
runtimeBinaryPath: string,
13+
entryPort: number,
14+
loopbackPort: number
15+
): Runtime;
16+
17+
isSupported(): boolean;
18+
supportSuggestion: string;
19+
description: string;
20+
distribution: string;
21+
}
22+
23+
const COMMON_RUNTIME_ARGS = ["serve", "--binary", "--verbose"];
24+
25+
function waitForExit(process: childProcess.ChildProcess): Promise<number> {
26+
return new Promise((resolve) => {
27+
process.once("exit", (code) => resolve(code ?? -1));
28+
});
29+
}
30+
31+
class NativeRuntime implements Runtime {
32+
static isSupported() {
33+
return process.platform === "linux"; // TODO: and "darwin"?
34+
}
35+
static supportSuggestion = "Run using a Linux or macOS based system";
36+
static description = "natively ⚡️";
37+
static distribution = `${process.platform}-${process.arch}`;
38+
39+
readonly #command: string;
40+
readonly #args: string[];
41+
42+
#process?: childProcess.ChildProcess;
43+
#processExitPromise?: Promise<number>;
44+
45+
constructor(
46+
protected readonly runtimeBinaryPath: string,
47+
protected readonly entryPort: number,
48+
protected readonly loopbackPort: number
49+
) {
50+
const [command, ...args] = this.getCommand();
51+
this.#command = command;
52+
this.#args = args;
53+
}
54+
55+
getCommand(): string[] {
56+
return [
57+
this.runtimeBinaryPath,
58+
...COMMON_RUNTIME_ARGS,
59+
`--socket-addr=${SOCKET_ENTRY}=127.0.0.1:${this.entryPort}`,
60+
`--external-addr=${SERVICE_LOOPBACK}=127.0.0.1:${this.loopbackPort}`,
61+
// TODO: consider adding support for unix sockets?
62+
// `--socket-fd=${SOCKET_ENTRY}=${this.entryPort}`,
63+
// `--external-addr=${SERVICE_LOOPBACK}=${this.loopbackPort}`,
64+
"-",
65+
];
66+
}
67+
68+
async updateConfig(configBuffer: Buffer) {
69+
// 1. Stop existing process (if any) and wait for exit
70+
await this.dispose();
71+
// TODO: what happens if runtime crashes?
72+
73+
// 2. Start new process
74+
const runtimeProcess = await childProcess.spawn(this.#command, this.#args, {
75+
stdio: "pipe",
76+
shell: true,
77+
});
78+
this.#process = runtimeProcess;
79+
this.#processExitPromise = waitForExit(runtimeProcess);
80+
81+
// TODO: may want to proxy these and prettify ✨
82+
// runtimeProcess.stdout.on("data", (data) => process.stdout.write(data));
83+
// runtimeProcess.stderr.on("data", (data) => process.stderr.write(data));
84+
runtimeProcess.stdout.pipe(process.stdout);
85+
runtimeProcess.stderr.pipe(process.stderr);
86+
87+
// 3. Write config
88+
runtimeProcess.stdin.write(configBuffer);
89+
}
90+
91+
async dispose() {
92+
this.#process?.kill();
93+
await this.#processExitPromise;
94+
}
95+
}
96+
97+
class WSLRuntime extends NativeRuntime {
98+
static isSupported() {
99+
return process.platform === "win32"; // TODO: && parse output from `wsl --list --verbose`, may need to check distro?;
100+
}
101+
static supportSuggestion =
102+
"Install the Windows Subsystem for Linux (https://aka.ms/wsl), " +
103+
"then run as you are at the moment";
104+
static description = "using WSL ✨";
105+
static distribution = `linux-${process.arch}`;
106+
107+
getCommand(): string[] {
108+
const command = super.getCommand();
109+
command.unshift("wsl"); // TODO: may need to select distro?
110+
// TODO: may need to convert runtime path to /mnt/c/...
111+
return command;
112+
}
113+
}
114+
115+
class DockerRuntime extends NativeRuntime {
116+
static isSupported() {
117+
const result = childProcess.spawnSync("docker", ["--version"]); // TODO: check daemon running too?
118+
return result.error === undefined;
119+
}
120+
static supportSuggestion =
121+
"Install Docker Desktop (https://www.docker.com/products/docker-desktop/), " +
122+
"then run as you are at the moment";
123+
static description = "using Docker 🐳";
124+
static distribution = `linux-${process.arch}`;
125+
126+
getCommand(): string[] {
127+
// TODO: consider reusing container, but just restarting process within
128+
return [
129+
"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+
];
143+
}
144+
}
145+
146+
const RUNTIMES = [NativeRuntime, WSLRuntime, DockerRuntime];
147+
let supportedRuntime: RuntimeConstructor;
148+
export function getSupportedRuntime(): RuntimeConstructor {
149+
// Return cached result to avoid checking support more than required
150+
if (supportedRuntime !== undefined) return supportedRuntime;
151+
152+
// Return and cache the best runtime (`RUNTIMES` is sorted by preference)
153+
for (const runtime of RUNTIMES) {
154+
if (runtime.isSupported()) {
155+
return (supportedRuntime = runtime);
156+
}
157+
}
158+
159+
// Throw with installation suggestions if we couldn't find a supported one
160+
const suggestions = RUNTIMES.map(
161+
({ supportSuggestion }) => `- ${supportSuggestion}`
162+
);
163+
throw new MiniflareError(
164+
"ERR_RUNTIME_UNSUPPORTED",
165+
`The 🦄 Cloudflare Workers Runtime 🦄 does not support your system (${
166+
process.platform
167+
} ${process.arch}). Either:\n${suggestions.join("\n")}\n`
168+
);
169+
}
170+
171+
export * from "./config";

0 commit comments

Comments
 (0)