@usine/cloudflare 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +95 -2
  3. package/dist/access.d.ts +12 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +32 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/api/api.d.ts +215 -0
  8. package/dist/api/api.d.ts.map +1 -0
  9. package/dist/api/api.js +180 -0
  10. package/dist/api/api.js.map +1 -0
  11. package/dist/api/harness.d.ts +49 -0
  12. package/dist/api/harness.d.ts.map +1 -0
  13. package/dist/api/harness.js +57 -0
  14. package/dist/api/harness.js.map +1 -0
  15. package/dist/api/names.d.ts +28 -0
  16. package/dist/api/names.d.ts.map +1 -0
  17. package/dist/api/names.js +31 -0
  18. package/dist/api/names.js.map +1 -0
  19. package/dist/api/stream.d.ts +65 -0
  20. package/dist/api/stream.d.ts.map +1 -0
  21. package/dist/api/stream.js +70 -0
  22. package/dist/api/stream.js.map +1 -0
  23. package/dist/registry.d.ts +28 -0
  24. package/dist/registry.d.ts.map +1 -0
  25. package/dist/registry.js +45 -0
  26. package/dist/registry.js.map +1 -0
  27. package/dist/sandbox/outbound.d.ts +53 -0
  28. package/dist/sandbox/outbound.d.ts.map +1 -0
  29. package/dist/sandbox/outbound.js +109 -0
  30. package/dist/sandbox/outbound.js.map +1 -0
  31. package/dist/sandbox/ports.d.ts +99 -0
  32. package/dist/sandbox/ports.d.ts.map +1 -0
  33. package/dist/sandbox/ports.js +66 -0
  34. package/dist/sandbox/ports.js.map +1 -0
  35. package/dist/sandbox/process.d.ts +55 -0
  36. package/dist/sandbox/process.d.ts.map +1 -0
  37. package/dist/sandbox/process.js +172 -0
  38. package/dist/sandbox/process.js.map +1 -0
  39. package/dist/sandbox/sandbox.d.ts +36 -0
  40. package/dist/sandbox/sandbox.d.ts.map +1 -0
  41. package/dist/sandbox/sandbox.js +387 -0
  42. package/dist/sandbox/sandbox.js.map +1 -0
  43. package/dist/sandbox/stream.d.ts +26 -0
  44. package/dist/sandbox/stream.d.ts.map +1 -0
  45. package/dist/sandbox/stream.js +167 -0
  46. package/dist/sandbox/stream.js.map +1 -0
  47. package/dist/worker.d.ts +77 -0
  48. package/dist/worker.d.ts.map +1 -0
  49. package/dist/worker.js +362 -0
  50. package/dist/worker.js.map +1 -0
  51. package/image/Dockerfile +43 -0
  52. package/image/codex-config.toml +10 -0
  53. package/image/init.sh +17 -0
  54. package/package.json +59 -3
  55. package/wrangler.jsonc +32 -0
@@ -0,0 +1,99 @@
1
+ import { CloudflareInstanceType } from "@usine/core/worker";
2
+ import { Context, type DateTime, type Duration, Effect, type Option, Schema } from "effect";
3
+ import { SandboxCredentials, SandboxName, SandboxSummary } from "../api/api.ts";
4
+ import type { Interception } from "./outbound.ts";
5
+ /** A command started in the container, with the streams the start requested. */
6
+ export interface ContainerProcess {
7
+ readonly stdin: WritableStream<Uint8Array> | null;
8
+ readonly stdout: ReadableStream<Uint8Array> | null;
9
+ readonly stderr: ReadableStream<Uint8Array> | null;
10
+ readonly exitCode: Promise<number>;
11
+ }
12
+ export declare const ContainerOperation: Schema.Literals<readonly ["start", "exec", "read", "destroy", "keep-alive", "intercept"]>;
13
+ export type ContainerOperation = typeof ContainerOperation.Type;
14
+ declare const ContainerFailure_base: Schema.Class<ContainerFailure, Schema.TaggedStruct<"ContainerFailure", {
15
+ readonly operation: Schema.Literals<readonly ["start", "exec", "read", "destroy", "keep-alive", "intercept"]>;
16
+ readonly message: Schema.String;
17
+ }>, import("effect/Cause").YieldableError>;
18
+ /** A Container API call failed. `message` comes from the runtime and contains no credential. */
19
+ export declare class ContainerFailure extends ContainerFailure_base {
20
+ }
21
+ declare const SandboxContainer_base: Context.ServiceClass<SandboxContainer, "@usine/cloudflare/SandboxContainer", {
22
+ readonly running: Effect.Effect<boolean>;
23
+ /** The Worker's `containers[].images`, by name. */
24
+ readonly images: Effect.Effect<Readonly<Record<string, string>>>;
25
+ /** Starts the container from `image`, an entry of `images`, without internet access. */
26
+ readonly start: (image: string, instanceType: CloudflareInstanceType) => Effect.Effect<void, ContainerFailure>;
27
+ /** Runs `command` as the image's user in its home directory, with `HOME` set and piped output. */
28
+ readonly exec: (command: ReadonlyArray<string>, stdin: "pipe" | "closed") => Effect.Effect<ContainerProcess, ContainerFailure>;
29
+ /** Returns the content of `path`, or `None` if it doesn't exist. */
30
+ readonly read: (path: string) => Effect.Effect<Option.Option<Uint8Array>, ContainerFailure>;
31
+ /** Returns the content of `path` after its first `offset` bytes, or `None` if it doesn't exist. */
32
+ readonly readFrom: (path: string, offset: number) => Effect.Effect<Option.Option<Uint8Array>, ContainerFailure>;
33
+ /** Returns the size of `path` in bytes, or `None` if it doesn't exist. */
34
+ readonly size: (path: string) => Effect.Effect<Option.Option<number>, ContainerFailure>;
35
+ readonly destroy: Effect.Effect<void, ContainerFailure>;
36
+ /** Sets the time without a request after which the platform stops the container. */
37
+ readonly keepAlive: (inactivity: Duration.Duration) => Effect.Effect<void, ContainerFailure>;
38
+ /** Sends the container's requests for the interception's host to its outbound handler. */
39
+ readonly intercept: (interception: Interception) => Effect.Effect<void, ContainerFailure>;
40
+ }>;
41
+ /** The Durable Object Container API calls a sandbox makes (`ctx.container` in the Worker). */
42
+ export declare class SandboxContainer extends SandboxContainer_base {
43
+ }
44
+ declare const StoredSandbox_base: Schema.Class<StoredSandbox, Schema.Struct<{
45
+ readonly name: Schema.brand<Schema.String, "SandboxName">;
46
+ readonly owner: Schema.brand<Schema.String, "SandboxOwner">;
47
+ readonly attempt: Schema.brand<Schema.NonEmptyString, "AttemptId">;
48
+ readonly image: Schema.brand<Schema.String, "CloudflareImageName">;
49
+ readonly instanceType: Schema.Literals<readonly ["lite", "basic", "standard-1", "standard-2", "standard-3", "standard-4"]>;
50
+ readonly createdAt: Schema.DateTimeUtc;
51
+ /**
52
+ * The keepalive alarm keeps the container running until this time. Each create, credential
53
+ * renewal, and command extends it.
54
+ */
55
+ readonly leaseUntil: Schema.DateTimeUtc;
56
+ /**
57
+ * Whether usine holds the sandbox for a checkout that it keeps. The keepalive alarm then keeps
58
+ * the container running past `leaseUntil`, until usine destroys the sandbox.
59
+ */
60
+ readonly held: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
61
+ /** Kept so the outbound handlers can be registered again after the Durable Object restarts. */
62
+ readonly credentials: typeof SandboxCredentials;
63
+ }>, {}>;
64
+ /** A sandbox as its Durable Object stores it. */
65
+ export declare class StoredSandbox extends StoredSandbox_base {
66
+ summary(running: boolean, draining: boolean): SandboxSummary;
67
+ }
68
+ declare const SandboxStorage_base: Context.ServiceClass<SandboxStorage, "@usine/cloudflare/SandboxStorage", {
69
+ readonly load: Effect.Effect<Option.Option<StoredSandbox>>;
70
+ /**
71
+ * Passes the stored sandbox, or `None`, to `change`, saves the sandbox that `change` returns
72
+ * with its result, and returns the result. Saves nothing when that sandbox is `None`. No other
73
+ * request runs between the read and the write.
74
+ */
75
+ readonly modify: <A>(change: (stored: Option.Option<StoredSandbox>) => readonly [A, Option.Option<StoredSandbox>]) => Effect.Effect<A>;
76
+ /** Deletes the stored sandbox and its alarm. */
77
+ readonly clear: Effect.Effect<void>;
78
+ /** Replaces the sandbox's alarm. */
79
+ readonly alarmAt: (at: DateTime.Utc) => Effect.Effect<void>;
80
+ }>;
81
+ /** The Durable Object storage of one sandbox. */
82
+ export declare class SandboxStorage extends SandboxStorage_base {
83
+ }
84
+ declare const SandboxDirectory_base: Context.ServiceClass<SandboxDirectory, "@usine/cloudflare/SandboxDirectory", {
85
+ readonly record: (sandbox: SandboxSummary) => Effect.Effect<void>;
86
+ readonly forget: (name: SandboxName) => Effect.Effect<void>;
87
+ }>;
88
+ /** The registry that lists the sandboxes of each usine Worker. */
89
+ export declare class SandboxDirectory extends SandboxDirectory_base {
90
+ }
91
+ declare const SandboxSettings_base: Context.ServiceClass<SandboxSettings, "@usine/cloudflare/SandboxSettings", {
92
+ /** The hosts the sandboxes reach over HTTPS without a credential, from `ALLOWED_HOSTS`. */
93
+ readonly allowedHosts: ReadonlyArray<string>;
94
+ }>;
95
+ /** The Worker's settings that every sandbox reads. */
96
+ export declare class SandboxSettings extends SandboxSettings_base {
97
+ }
98
+ export {};
99
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../../src/sandbox/ports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import { AttemptId } from "@usine/core/attempt";
2
+ import { CloudflareImageName, CloudflareInstanceType } from "@usine/core/worker";
3
+ import { Context, Effect, Schema } from "effect";
4
+ import { SandboxCredentials, SandboxName, SandboxOwner, SandboxSummary } from "../api/api.js";
5
+ export const ContainerOperation = Schema.Literals([
6
+ "start",
7
+ "exec",
8
+ "read",
9
+ "destroy",
10
+ "keep-alive",
11
+ "intercept",
12
+ ]);
13
+ /** A Container API call failed. `message` comes from the runtime and contains no credential. */
14
+ export class ContainerFailure extends Schema.TaggedError()("ContainerFailure", {
15
+ operation: ContainerOperation,
16
+ message: Schema.String,
17
+ }) {
18
+ }
19
+ /** The Durable Object Container API calls a sandbox makes (`ctx.container` in the Worker). */
20
+ export class SandboxContainer extends Context.Service()("@usine/cloudflare/SandboxContainer") {
21
+ }
22
+ /** A sandbox as its Durable Object stores it. */
23
+ export class StoredSandbox extends Schema.Class("@usine/cloudflare/StoredSandbox")({
24
+ name: SandboxName,
25
+ owner: SandboxOwner,
26
+ attempt: AttemptId,
27
+ image: CloudflareImageName,
28
+ instanceType: CloudflareInstanceType,
29
+ createdAt: Schema.DateTimeUtc,
30
+ /**
31
+ * The keepalive alarm keeps the container running until this time. Each create, credential
32
+ * renewal, and command extends it.
33
+ */
34
+ leaseUntil: Schema.DateTimeUtc,
35
+ /**
36
+ * Whether usine holds the sandbox for a checkout that it keeps. The keepalive alarm then keeps
37
+ * the container running past `leaseUntil`, until usine destroys the sandbox.
38
+ */
39
+ held: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))),
40
+ /** Kept so the outbound handlers can be registered again after the Durable Object restarts. */
41
+ credentials: SandboxCredentials,
42
+ }) {
43
+ summary(running, draining) {
44
+ return new SandboxSummary({
45
+ name: this.name,
46
+ owner: this.owner,
47
+ attempt: this.attempt,
48
+ image: this.image,
49
+ instanceType: this.instanceType,
50
+ createdAt: this.createdAt,
51
+ running,
52
+ held: this.held,
53
+ draining,
54
+ });
55
+ }
56
+ }
57
+ /** The Durable Object storage of one sandbox. */
58
+ export class SandboxStorage extends Context.Service()("@usine/cloudflare/SandboxStorage") {
59
+ }
60
+ /** The registry that lists the sandboxes of each usine Worker. */
61
+ export class SandboxDirectory extends Context.Service()("@usine/cloudflare/SandboxDirectory") {
62
+ }
63
+ /** The Worker's settings that every sandbox reads. */
64
+ export class SandboxSettings extends Context.Service()("@usine/cloudflare/SandboxSettings") {
65
+ }
66
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../../src/sandbox/ports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,OAAO,EAAgC,MAAM,EAAe,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAW9F,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC,QAAQ,CAAC;IAChD,OAAO;IACP,MAAM;IACN,MAAM;IACN,SAAS;IACT,YAAY;IACZ,WAAW;CACZ,CAAC,CAAC;AAGH,gGAAgG;AAChG,MAAM,OAAO,gBAAiB,SAAQ,MAAM,CAAC,WAAW,EAAoB,CAAC,kBAAkB,EAAE;IAC/F,SAAS,EAAE,kBAAkB;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM;CACvB,CAAC;CAAG;AAEL,8FAA8F;AAC9F,MAAM,OAAO,gBAAiB,SAAQ,OAAO,CAAC,OAAO,EA+BlD,CAAC,oCAAoC,CAAC;CAAG;AAE5C,iDAAiD;AACjD,MAAM,OAAO,aAAc,SAAQ,MAAM,CAAC,KAAK,CAAgB,iCAAiC,CAAC,CAAC;IAChG,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,YAAY;IACnB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,mBAAmB;IAC1B,YAAY,EAAE,sBAAsB;IACpC,SAAS,EAAE,MAAM,CAAC,WAAW;IAC7B;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC,WAAW;IAC9B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/E,+FAA+F;IAC/F,WAAW,EAAE,kBAAkB;CAChC,CAAC;IACA,OAAO,CAAC,OAAgB,EAAE,QAAiB;QACzC,OAAO,IAAI,cAAc,CAAC;YACxB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,OAAO;YACP,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,QAAQ;SACT,CAAC,CAAC;IACL,CAAC;CACF;AAED,iDAAiD;AACjD,MAAM,OAAO,cAAe,SAAQ,OAAO,CAAC,OAAO,EAiBhD,CAAC,kCAAkC,CAAC;CAAG;AAE1C,kEAAkE;AAClE,MAAM,OAAO,gBAAiB,SAAQ,OAAO,CAAC,OAAO,EAMlD,CAAC,oCAAoC,CAAC;CAAG;AAE5C,sDAAsD;AACtD,MAAM,OAAO,eAAgB,SAAQ,OAAO,CAAC,OAAO,EAMjD,CAAC,mCAAmC,CAAC;CAAG"}
@@ -0,0 +1,55 @@
1
+ import { Option } from "effect";
2
+ import { ProcessId } from "../api/api.ts";
3
+ /**
4
+ * Returns the Container API options of a command the Worker runs. The platform passes a command
5
+ * only the image's `PATH`, not its `ENV HOME`, so the options set `HOME` and start the command in
6
+ * the sandbox home.
7
+ */
8
+ export declare const commandOptions: (stdin: "pipe" | "closed") => ContainerExecOptions;
9
+ /** The files a command leaves in its process directory. */
10
+ export declare const processFiles: (process: ProcessId) => {
11
+ /** The command's process ID, which is also its process group and session. */
12
+ readonly pid: `${string}/pid`;
13
+ /** The exit code, written once the command exits. */
14
+ readonly exit: `${string}/exit`;
15
+ readonly stdout: `${string}/stdout`;
16
+ readonly stderr: `${string}/stderr`;
17
+ };
18
+ /**
19
+ * A POSIX shell script that runs the command line `$2` with process files in the directory `$1`,
20
+ * and passes its stdin, stdout, stderr, and exit code through. Run it with `sh -c`.
21
+ *
22
+ * The command runs in its own session and writes its process ID to `pid`. The command and every
23
+ * process that it starts get `USINE_PROCESS`, the directory, in their environment, so that
24
+ * `STOP_SCRIPT` finds them even after the command exits or they leave its session. `tee -p` copies
25
+ * its output to `stdout` and `stderr` and keeps copying if the stream that started it closes, so a
26
+ * later request can attach. The exit code goes to `exit` when the command ends.
27
+ */
28
+ export declare const PROCESS_SCRIPT: string;
29
+ /** Returns the container command that runs `command` through `PROCESS_SCRIPT`. */
30
+ export declare const processCommand: (process: ProcessId, command: string) => ReadonlyArray<string>;
31
+ /**
32
+ * A bash script that stops what the commands in the processes directory `$1` started: every
33
+ * process in the session of each command, and in the session of each process whose
34
+ * `USINE_PROCESS` is in `$1`, so it includes what a command left running after it exited. It sends
35
+ * TERM to each process group in those sessions, waits up to five seconds, then sends KILL until no
36
+ * process is left. It prints the number of sessions it signaled, and fails if a process still runs
37
+ * five seconds after the first KILL.
38
+ *
39
+ * A command that exited has released its process ID. If another process has that ID now, the
40
+ * command's session has ended, and the script skips the session that has the ID now.
41
+ */
42
+ export declare const STOP_SCRIPT: string;
43
+ /** Returns the container command that runs `STOP_SCRIPT`. */
44
+ export declare const stopCommand: ReadonlyArray<string>;
45
+ /** Returns the container command that prints the bytes of `file` from `offset`. */
46
+ export declare const readFromCommand: (file: string, offset: number) => ReadonlyArray<string>;
47
+ /** Returns the number that `STOP_SCRIPT` printed, or `None` for any other output. */
48
+ export declare const stoppedCount: (output: string) => Option.Option<number>;
49
+ /** Returns the process ID in a `pid` file, or `None` if it's empty or malformed. */
50
+ export declare const pidOf: (content: string) => Option.Option<number>;
51
+ /** Returns the exit code in an `exit` file, or `None` if it's malformed. */
52
+ export declare const exitCodeOf: (content: string) => Option.Option<number>;
53
+ /** Returns a new random process ID. */
54
+ export declare const newProcessId: () => ProcessId;
55
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/sandbox/process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG1C;;;;GAIG;AACH,eAAO,MAAM,cAAc,UAAW,MAAM,GAAG,QAAQ,KAAG,oBAQzD,CAAC;AAKF,2DAA2D;AAC3D,eAAO,MAAM,YAAY,YAAa,SAAS;IAG3C,6EAA6E;aAC7E,GAAG;IACH,qDAAqD;aACrD,IAAI;aACJ,MAAM;aACN,MAAM;CAET,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,QAenB,CAAC;AAET,kFAAkF;AAClF,eAAO,MAAM,cAAc,YAAa,SAAS,WAAW,MAAM,KAAG,aAAa,CAAC,MAAM,CAOxF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,QA8DhB,CAAC;AAET,6DAA6D;AAC7D,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,MAAM,CAM7C,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,eAAe,SAAU,MAAM,UAAU,MAAM,KAAG,aAAa,CAAC,MAAM,CAMlF,CAAC;AAMF,qFAAqF;AACrF,eAAO,MAAM,YAAY,WAAY,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAA+B,CAAC;AAMlG,oFAAoF;AACpF,eAAO,MAAM,KAAK,YAAa,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAA8B,CAAC;AAM3F,4EAA4E;AAC5E,eAAO,MAAM,UAAU,YAAa,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CACjC,CAAC;AAIjC,uCAAuC;AACvC,eAAO,MAAM,YAAY,QAAO,SAG/B,CAAC"}
@@ -0,0 +1,172 @@
1
+ import { Option, Schema } from "effect";
2
+ import { ProcessId } from "../api/api.js";
3
+ import { PROCESSES_DIRECTORY, SANDBOX_HOME } from "../api/harness.js";
4
+ /**
5
+ * Returns the Container API options of a command the Worker runs. The platform passes a command
6
+ * only the image's `PATH`, not its `ENV HOME`, so the options set `HOME` and start the command in
7
+ * the sandbox home.
8
+ */
9
+ export const commandOptions = (stdin) => {
10
+ const options = {
11
+ cwd: SANDBOX_HOME,
12
+ env: { HOME: SANDBOX_HOME },
13
+ stdout: "pipe",
14
+ stderr: "pipe",
15
+ };
16
+ return stdin === "pipe" ? { ...options, stdin: "pipe" } : options;
17
+ };
18
+ /** Returns the directory with the process files of `process`. */
19
+ const processDirectory = (process) => `${PROCESSES_DIRECTORY}/${process}`;
20
+ /** The files a command leaves in its process directory. */
21
+ export const processFiles = (process) => {
22
+ const directory = processDirectory(process);
23
+ return {
24
+ /** The command's process ID, which is also its process group and session. */
25
+ pid: `${directory}/pid`,
26
+ /** The exit code, written once the command exits. */
27
+ exit: `${directory}/exit`,
28
+ stdout: `${directory}/stdout`,
29
+ stderr: `${directory}/stderr`,
30
+ };
31
+ };
32
+ /**
33
+ * A POSIX shell script that runs the command line `$2` with process files in the directory `$1`,
34
+ * and passes its stdin, stdout, stderr, and exit code through. Run it with `sh -c`.
35
+ *
36
+ * The command runs in its own session and writes its process ID to `pid`. The command and every
37
+ * process that it starts get `USINE_PROCESS`, the directory, in their environment, so that
38
+ * `STOP_SCRIPT` finds them even after the command exits or they leave its session. `tee -p` copies
39
+ * its output to `stdout` and `stderr` and keeps copying if the stream that started it closes, so a
40
+ * later request can attach. The exit code goes to `exit` when the command ends.
41
+ */
42
+ export const PROCESS_SCRIPT = String.raw `
43
+ d=$1
44
+ mkdir -p "$d" && mkfifo "$d/stdout.pipe" "$d/stderr.pipe" || exit 125
45
+ exec 3<&0
46
+ tee -p -a "$d/stdout" < "$d/stdout.pipe" &
47
+ tee -p -a "$d/stderr" < "$d/stderr.pipe" >&2 &
48
+ USINE_PROCESS=$d setsid -w sh -c 'printf %s "$$" > "$0" && exec sh -c "$1"' "$d/pid" "$2" <&3 > "$d/stdout.pipe" 2> "$d/stderr.pipe" &
49
+ command=$!
50
+ exec 3<&-
51
+ wait "$command"
52
+ code=$?
53
+ wait
54
+ printf %s "$code" > "$d/exit.next" && mv "$d/exit.next" "$d/exit"
55
+ rm -f "$d/stdout.pipe" "$d/stderr.pipe"
56
+ exit "$code"
57
+ `.trim();
58
+ /** Returns the container command that runs `command` through `PROCESS_SCRIPT`. */
59
+ export const processCommand = (process, command) => [
60
+ "sh",
61
+ "-c",
62
+ PROCESS_SCRIPT,
63
+ "usine-process",
64
+ processDirectory(process),
65
+ command,
66
+ ];
67
+ /**
68
+ * A bash script that stops what the commands in the processes directory `$1` started: every
69
+ * process in the session of each command, and in the session of each process whose
70
+ * `USINE_PROCESS` is in `$1`, so it includes what a command left running after it exited. It sends
71
+ * TERM to each process group in those sessions, waits up to five seconds, then sends KILL until no
72
+ * process is left. It prints the number of sessions it signaled, and fails if a process still runs
73
+ * five seconds after the first KILL.
74
+ *
75
+ * A command that exited has released its process ID. If another process has that ID now, the
76
+ * command's session has ended, and the script skips the session that has the ID now.
77
+ */
78
+ export const STOP_SCRIPT = `
79
+ root=$1
80
+ declare -A groups=() sessions=()
81
+ read -r line < "/proc/$$/stat"
82
+ fields=(\${line##*") "})
83
+ own=\${fields[3]}
84
+ scan() {
85
+ local process line fields entry
86
+ groups=()
87
+ for process in /proc/[0-9]*; do
88
+ read -r line 2>/dev/null < "$process/stat" || continue
89
+ fields=(\${line##*") "})
90
+ case \${fields[0]} in Z|X) continue ;; esac
91
+ groups[\${fields[3]}]+=" \${fields[2]}"
92
+ [ "\${fields[3]}" = "$own" ] && continue
93
+ while IFS= read -r -d '' entry; do
94
+ case $entry in "USINE_PROCESS=$root"/*) sessions[\${fields[3]}]=1; break ;; esac
95
+ done 2>/dev/null < "$process/environ"
96
+ done
97
+ }
98
+ signal() {
99
+ local session group
100
+ local -A sent=()
101
+ for session in "\${!sessions[@]}"; do
102
+ for group in \${groups[$session]-}; do
103
+ [ -n "\${sent[$group]-}" ] || kill "-$1" -- "-$group" 2>/dev/null
104
+ sent[$group]=1
105
+ done
106
+ done
107
+ }
108
+ running() {
109
+ local session
110
+ for session in "\${!sessions[@]}"; do
111
+ [ -n "\${groups[$session]-}" ] && echo "$session"
112
+ done
113
+ }
114
+ scan
115
+ for pidfile in "$root"/*/pid; do
116
+ [ -f "$pidfile" ] || continue
117
+ read -r pid < "$pidfile"
118
+ case "$pid" in ''|*[!0-9]*) continue ;; esac
119
+ [ -f "\${pidfile%/pid}/exit" ] && [ -e "/proc/$pid" ] && continue
120
+ [ "$pid" != "$own" ] && [ -n "\${groups[$pid]-}" ] && sessions[$pid]=1
121
+ done
122
+ signal TERM
123
+ for _ in {1..20}; do
124
+ [ -z "$(running)" ] && break
125
+ sleep 0.25
126
+ scan
127
+ done
128
+ for _ in {1..20}; do
129
+ [ -z "$(running)" ] && break
130
+ signal KILL
131
+ sleep 0.25
132
+ scan
133
+ done
134
+ left=$(running)
135
+ if [ -n "$left" ]; then
136
+ echo "Processes still run after KILL in sessions" $left >&2
137
+ exit 1
138
+ fi
139
+ echo "\${#sessions[@]}"
140
+ `.trim();
141
+ /** Returns the container command that runs `STOP_SCRIPT`. */
142
+ export const stopCommand = [
143
+ "bash",
144
+ "-c",
145
+ STOP_SCRIPT,
146
+ "usine-stop",
147
+ PROCESSES_DIRECTORY,
148
+ ];
149
+ /** Returns the container command that prints the bytes of `file` from `offset`. */
150
+ export const readFromCommand = (file, offset) => [
151
+ "tail",
152
+ "-c",
153
+ `+${String(offset + 1)}`,
154
+ "--",
155
+ file,
156
+ ];
157
+ const decodeCount = Schema.decodeUnknownOption(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)));
158
+ /** Returns the number that `STOP_SCRIPT` printed, or `None` for any other output. */
159
+ export const stoppedCount = (output) => decodeCount(output.trim());
160
+ const decodePid = Schema.decodeUnknownOption(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1)));
161
+ /** Returns the process ID in a `pid` file, or `None` if it's empty or malformed. */
162
+ export const pidOf = (content) => decodePid(content.trim());
163
+ const decodeExitCode = Schema.decodeUnknownOption(Schema.NumberFromString.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 255 })));
164
+ /** Returns the exit code in an `exit` file, or `None` if it's malformed. */
165
+ export const exitCodeOf = (content) => decodeExitCode(content.trim());
166
+ const ProcessIdBytes = 16;
167
+ /** Returns a new random process ID. */
168
+ export const newProcessId = () => {
169
+ const bytes = crypto.getRandomValues(new Uint8Array(ProcessIdBytes));
170
+ return ProcessId.make([...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join(""));
171
+ };
172
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../../src/sandbox/process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { Duration, Effect, Layer } from "effect";
2
+ import { ContainerUnavailable, SandboxNotFound } from "../api/api.ts";
3
+ import { ContainerFailure, type ContainerProcess, SandboxContainer, SandboxDirectory, SandboxSettings, SandboxStorage, StoredSandbox } from "./ports.ts";
4
+ /** How long the keepalive alarm keeps a container running after the last create, renewal, or command. */
5
+ export declare const LEASE: Duration.Duration;
6
+ /** Waits for `process` to exit and returns its output bytes, unchanged, and exit code. */
7
+ export declare const collectBytes: (process: ContainerProcess) => Effect.Effect<{
8
+ stdout: Uint8Array<ArrayBufferLike>;
9
+ stderr: Uint8Array<ArrayBufferLike>;
10
+ exitCode: number;
11
+ }, ContainerFailure, never>;
12
+ /** Returns the stored sandbox if its container runs, or fails with `ContainerUnavailable`. */
13
+ export declare const runningSandboxFor: (name: string & import("effect/Brand").Brand<"SandboxName">) => Effect.Effect<StoredSandbox, ContainerUnavailable | SandboxNotFound, SandboxContainer | SandboxStorage>;
14
+ /**
15
+ * Extends the sandbox's lease from now and returns the stored sandbox, or fails with
16
+ * `SandboxNotFound` if it was destroyed.
17
+ */
18
+ export declare const extendLease: (name: string & import("effect/Brand").Brand<"SandboxName">) => Effect.Effect<StoredSandbox, SandboxNotFound, SandboxStorage>;
19
+ /**
20
+ * Keeps the container running while the sandbox's lease lasts or usine holds it, then lets the
21
+ * platform stop it after `INACTIVITY_TIMEOUT`. Runs from the Durable Object's alarm, and sets the
22
+ * next one.
23
+ */
24
+ export declare const keepAlive: () => Effect.Effect<void, never, SandboxContainer | SandboxStorage>;
25
+ /**
26
+ * Registers the outbound handlers and sets the inactivity timeout again after the Durable Object
27
+ * restarts while its container runs, since the platform resets both for each instance. Does
28
+ * nothing for a sandbox without a running container.
29
+ */
30
+ export declare const restore: () => Effect.Effect<void, never, SandboxContainer | SandboxSettings | SandboxStorage>;
31
+ /**
32
+ * The health and sandbox routes of `SandboxApi`, served by each sandbox's Durable Object. Requires
33
+ * `SandboxContainer`, `SandboxStorage`, `SandboxDirectory`, and `SandboxSettings`.
34
+ */
35
+ export declare const sandboxRoutes: Layer.Layer<never, never, import("effect/http/HttpRouter").HttpRouter | import("effect/http/HttpRouter").Request<"Requires", SandboxDirectory> | import("effect/http/HttpRouter").Request<"Requires", SandboxSettings> | (import("effect/http/HttpRouter").Request<"Requires", SandboxContainer> | import("effect/http/HttpRouter").Request<"Requires", SandboxStorage>)>;
36
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/sandbox.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,QAAQ,EAAE,MAAM,EAAE,KAAK,EAA6B,MAAM,QAAQ,CAAC;AAGtF,OAAO,EACL,oBAAoB,EAgBpB,eAAe,EAGhB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,yGAAyG;AACzG,eAAO,MAAM,KAAK,mBAAoB,CAAC;AAevC,0FAA0F;AAC1F,eAAO,MAAM,YAAY,YAAa,gBAAgB;;;;2BAQ+B,CAAC;AA+CtF,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,yKAS5B,CAAC;AAsDH;;;GAGG;AACH,eAAO,MAAM,WAAW,+HAMtB,CAAC;AAmOH;;;;GAIG;AACH,eAAO,MAAM,SAAS,qEAiCpB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,OAAO,uFA2BlB,CAAC;AAyBH;;;GAGG;AACH,eAAO,MAAM,aAAa,2WAGzB,CAAC"}