@descryy/runtime-controller 0.0.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.
- package/dist/capability-registry.d.ts +24 -0
- package/dist/capability-registry.d.ts.map +1 -0
- package/dist/capability-registry.js +42 -0
- package/dist/capability-registry.js.map +1 -0
- package/dist/collector-version.d.ts +10 -0
- package/dist/collector-version.d.ts.map +1 -0
- package/dist/collector-version.js +12 -0
- package/dist/collector-version.js.map +1 -0
- package/dist/container-sandbox.d.ts +188 -0
- package/dist/container-sandbox.d.ts.map +1 -0
- package/dist/container-sandbox.js +233 -0
- package/dist/container-sandbox.js.map +1 -0
- package/dist/controller.d.ts +162 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +433 -0
- package/dist/controller.js.map +1 -0
- package/dist/dependency-version-check.d.ts +90 -0
- package/dist/dependency-version-check.d.ts.map +1 -0
- package/dist/dependency-version-check.js +121 -0
- package/dist/dependency-version-check.js.map +1 -0
- package/dist/env.d.ts +16 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +18 -0
- package/dist/env.js.map +1 -0
- package/dist/environment-metadata.d.ts +23 -0
- package/dist/environment-metadata.d.ts.map +1 -0
- package/dist/environment-metadata.js +80 -0
- package/dist/environment-metadata.js.map +1 -0
- package/dist/environment-version-check.d.ts +83 -0
- package/dist/environment-version-check.d.ts.map +1 -0
- package/dist/environment-version-check.js +218 -0
- package/dist/environment-version-check.js.map +1 -0
- package/dist/execution-safety.d.ts +154 -0
- package/dist/execution-safety.d.ts.map +1 -0
- package/dist/execution-safety.js +194 -0
- package/dist/execution-safety.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestration.d.ts +70 -0
- package/dist/orchestration.d.ts.map +1 -0
- package/dist/orchestration.js +255 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/process-collector.d.ts +34 -0
- package/dist/process-collector.d.ts.map +1 -0
- package/dist/process-collector.js +148 -0
- package/dist/process-collector.js.map +1 -0
- package/dist/process-manager.d.ts +133 -0
- package/dist/process-manager.d.ts.map +1 -0
- package/dist/process-manager.js +333 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/readiness.d.ts +120 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +179 -0
- package/dist/readiness.js.map +1 -0
- package/dist/sandbox.d.ts +139 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +271 -0
- package/dist/sandbox.js.map +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process manager (plan §5): command execution, working directory,
|
|
3
|
+
* environment variables, process-group tracking, stdout/stderr capture,
|
|
4
|
+
* exit-code capture, cleanup with a shutdown grace period, and orphan-
|
|
5
|
+
* process prevention.
|
|
6
|
+
*
|
|
7
|
+
* Orphan prevention and group-kill use POSIX process groups
|
|
8
|
+
* (`detached: true` + `process.kill(-pid, signal)`), matching this
|
|
9
|
+
* project's Linux development and CI targets. Not portable to Windows as
|
|
10
|
+
* written — out of scope for the V1 fixture-app slice (plan §34).
|
|
11
|
+
*/
|
|
12
|
+
import type { FilesystemPolicy, NetworkPolicy, ProcessHandle, ResourceLimits, ServiceAttachConfiguration } from "@descryy/runtime-contracts";
|
|
13
|
+
/**
|
|
14
|
+
* Binds port 0 (OS picks a free one), reads it, releases it immediately.
|
|
15
|
+
* A real gap remains between "free when checked" and "still free when the
|
|
16
|
+
* real service binds it" (RT-024's own class of gap, one layer down) --
|
|
17
|
+
* unavoidable without binding the real service directly to the probe
|
|
18
|
+
* socket, which `spawnProcess`'s child-process model doesn't support, and
|
|
19
|
+
* which no ordinary application would cooperate with anyway.
|
|
20
|
+
*
|
|
21
|
+
* **Examined for a fix and deliberately not "fixed".** Holding the probe
|
|
22
|
+
* socket open for longer does not narrow the window, it widens it: the
|
|
23
|
+
* child cannot bind while we hold the port, and the dominant term is the
|
|
24
|
+
* child's own startup, not our release. Retrying on the child's bind error
|
|
25
|
+
* would mean parsing that error out of process output, which is
|
|
26
|
+
* per-language text and has no place in a language-neutral controller.
|
|
27
|
+
* What was done instead is to make the one symptom legible: `awaitReadiness`
|
|
28
|
+
* now names the endpoint it probed, so a collision reads as "nothing
|
|
29
|
+
* answered on this port" rather than as an unexplained application failure.
|
|
30
|
+
* In practice the window is a handful of milliseconds and readiness always
|
|
31
|
+
* runs afterwards, so a genuine collision fails loudly -- it is just no
|
|
32
|
+
* longer misattributed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function allocateEphemeralPort(): Promise<number>;
|
|
35
|
+
export interface SpawnProcessOptions {
|
|
36
|
+
readonly processId: string;
|
|
37
|
+
readonly command: string;
|
|
38
|
+
readonly args?: readonly string[];
|
|
39
|
+
readonly cwd?: string;
|
|
40
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
41
|
+
/** Joins the resulting ProcessHandle back to ExecutionConfiguration.services (RT-023). Omit for a process with no corresponding named service. */
|
|
42
|
+
readonly serviceName?: string;
|
|
43
|
+
/** The real port this process is bound to, when the caller already knows it (e.g. an ephemeral port it allocated before spawning). Recorded on the handle, not otherwise used by this function. */
|
|
44
|
+
readonly port?: number;
|
|
45
|
+
/** §21's execution boundary (RT-070). Applied via `applyResourceLimits` -- see its own comment for the refuse-rather-than-silently-unconstrained contract. Re-applied on every (re)spawn, including `restart()`. */
|
|
46
|
+
readonly resourceLimits?: ResourceLimits;
|
|
47
|
+
/** §21's execution boundary, filesystem half. Applied via `applySandbox` -- see its own comment. Re-applied on every (re)spawn, including `restart()`. */
|
|
48
|
+
readonly filesystemPolicy?: FilesystemPolicy;
|
|
49
|
+
/** §21's execution boundary, network half. Applied via `applySandbox` -- see its own comment. Re-applied on every (re)spawn, including `restart()`. */
|
|
50
|
+
readonly networkPolicy?: NetworkPolicy;
|
|
51
|
+
/**
|
|
52
|
+
* Selects which real mechanism enforces `filesystemPolicy`/`networkPolicy`.
|
|
53
|
+
* Absent means `"bwrap"` -- exactly today's behavior, unchanged.
|
|
54
|
+
* `"container"` routes through `applyContainerSandbox` (`./container-sandbox.ts`)
|
|
55
|
+
* instead. See `ExecutionConfiguration.sandboxBackend`'s own comment.
|
|
56
|
+
*
|
|
57
|
+
* **Does not compose with `resourceLimits` in this pass**: the container
|
|
58
|
+
* backend applies directly to `command`/`args`, skipping
|
|
59
|
+
* `applyResourceLimits`'s `prlimit` wrap entirely (a minimal container
|
|
60
|
+
* image is not guaranteed to carry `prlimit`, and Docker's own
|
|
61
|
+
* `--memory`/`--cpus`/`--pids-limit` equivalent is not wired up here) --
|
|
62
|
+
* disclosed in `container-sandbox.ts`'s own module doc, not silently
|
|
63
|
+
* dropped.
|
|
64
|
+
*/
|
|
65
|
+
readonly sandboxBackend?: "bwrap" | "container";
|
|
66
|
+
}
|
|
67
|
+
export interface ProcessExitInfo {
|
|
68
|
+
readonly exitCode: number | null;
|
|
69
|
+
readonly signal: NodeJS.Signals | null;
|
|
70
|
+
}
|
|
71
|
+
export interface ManagedProcess {
|
|
72
|
+
handle(): ProcessHandle;
|
|
73
|
+
/** Combined stdout+stderr captured so far, in arrival order — the backing store for log-pattern readiness checks. */
|
|
74
|
+
readOutput(): string;
|
|
75
|
+
waitForExit(): Promise<ProcessExitInfo>;
|
|
76
|
+
/** SIGTERM the process group, wait up to `gracePeriodMs`, then SIGKILL if still alive. Idempotent. */
|
|
77
|
+
kill(gracePeriodMs?: number): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Kills the current process (if still alive — a no-op wait if it already
|
|
80
|
+
* exited on its own) and spawns a fresh one with the SAME `command`/
|
|
81
|
+
* `args`/`cwd`/`env`. `handle()`, `readOutput()` and `waitForExit()` all
|
|
82
|
+
* observe the new process afterward — this is the same `ManagedProcess`
|
|
83
|
+
* object, not a new one, so a caller holding the original reference does
|
|
84
|
+
* not need to re-fetch anything. Readiness is the caller's job, same as
|
|
85
|
+
* after the original `spawnProcess` — this resolves once the new process
|
|
86
|
+
* EXISTS, not once it is ready.
|
|
87
|
+
*/
|
|
88
|
+
restart(gracePeriodMs?: number): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
export declare function spawnProcess(options: SpawnProcessOptions): ManagedProcess;
|
|
91
|
+
/**
|
|
92
|
+
* `process.kill(pid, 0)` sends no signal — it only asks the kernel whether
|
|
93
|
+
* delivery would be possible. `ESRCH` means no such pid; `EPERM` means a
|
|
94
|
+
* pid we don't own but that unambiguously exists, still alive for this
|
|
95
|
+
* purpose. Any other error is treated as "exists" rather than guessed away.
|
|
96
|
+
*
|
|
97
|
+
* A second, deliberate copy of `packages/orchestrator/src/attach-to-running-process.ts`'s
|
|
98
|
+
* function of the same name, not an import of it: `orchestrator` depends on
|
|
99
|
+
* `controller` (see `orchestrator/package.json`), so the reverse import
|
|
100
|
+
* would be circular. Tiny and pure, the same "duplicated because both
|
|
101
|
+
* copies are easy to notice drift" precedent `attach-to-running-jvm-process.ts`'s
|
|
102
|
+
* own `reconstructJvmSourcePath` already sets for exactly this reason.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
105
|
+
export interface AttachProcessOptions {
|
|
106
|
+
readonly processId: string;
|
|
107
|
+
readonly attach: ServiceAttachConfiguration;
|
|
108
|
+
readonly serviceName?: string;
|
|
109
|
+
readonly port?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `ManagedProcess` for a service `ExecutionController.run()` attaches to
|
|
113
|
+
* instead of spawning (`ServiceConfiguration.attach`). Reads
|
|
114
|
+
* `attach.logFilePath` fresh on every `readOutput()` call — the same
|
|
115
|
+
* growing-whole-buffer contract a spawned process's in-memory pipe buffer
|
|
116
|
+
* already has (`spawnProcess`'s own `outputBuffer`), so `createPollingProcessOutputSource`
|
|
117
|
+
* (`@descryy/runtime-orchestrator`) needs no attach-specific branch to
|
|
118
|
+
* consume either one.
|
|
119
|
+
*
|
|
120
|
+
* **`kill()` is a deliberate no-op, not a stub.** Descry did not spawn this
|
|
121
|
+
* process and `ATTACH_MODE_SCOPE_DISCLOSURE` (`@descryy/runtime-contracts`)
|
|
122
|
+
* is unconditional: attach mode "never executes code in it or applies
|
|
123
|
+
* changes to it." `ExecutionController`'s cleanup path kills every managed
|
|
124
|
+
* process on failure/timeout/stop — for an attached service that would mean
|
|
125
|
+
* terminating a process this runtime does not own, silently, the exact
|
|
126
|
+
* thing the disclosure promises never happens. Ending observation (no
|
|
127
|
+
* further polling) is the honest equivalent of "stop" here; the target
|
|
128
|
+
* keeps running, as it was before Descry ever attached. `restart()` throws
|
|
129
|
+
* for the same reason: there is no command this runtime could relaunch it
|
|
130
|
+
* with.
|
|
131
|
+
*/
|
|
132
|
+
export declare function attachManagedProcess(options: AttachProcessOptions): ManagedProcess;
|
|
133
|
+
//# sourceMappingURL=process-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../src/process-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAK7I;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC,CAevD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,kJAAkJ;IAClJ,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,mMAAmM;IACnM,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,oNAAoN;IACpN,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC,0JAA0J;IAC1J,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,uJAAuJ;IACvJ,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;CACjD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,IAAI,aAAa,CAAC;IACxB,qHAAqH;IACrH,UAAU,IAAI,MAAM,CAAC;IACrB,WAAW,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IACxC,sGAAsG;IACtG,IAAI,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;;;;OASG;IACH,OAAO,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAID,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,cAAc,CAiKzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAC;IAC5C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,GAAG,cAAc,CA4ElF"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process manager (plan §5): command execution, working directory,
|
|
3
|
+
* environment variables, process-group tracking, stdout/stderr capture,
|
|
4
|
+
* exit-code capture, cleanup with a shutdown grace period, and orphan-
|
|
5
|
+
* process prevention.
|
|
6
|
+
*
|
|
7
|
+
* Orphan prevention and group-kill use POSIX process groups
|
|
8
|
+
* (`detached: true` + `process.kill(-pid, signal)`), matching this
|
|
9
|
+
* project's Linux development and CI targets. Not portable to Windows as
|
|
10
|
+
* written — out of scope for the V1 fixture-app slice (plan §34).
|
|
11
|
+
*/
|
|
12
|
+
import { execFile, spawn } from "node:child_process";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { createServer } from "node:net";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { applyResourceLimits } from "./execution-safety.js";
|
|
17
|
+
import { applySandbox } from "./sandbox.js";
|
|
18
|
+
import { applyContainerSandbox } from "./container-sandbox.js";
|
|
19
|
+
/**
|
|
20
|
+
* Binds port 0 (OS picks a free one), reads it, releases it immediately.
|
|
21
|
+
* A real gap remains between "free when checked" and "still free when the
|
|
22
|
+
* real service binds it" (RT-024's own class of gap, one layer down) --
|
|
23
|
+
* unavoidable without binding the real service directly to the probe
|
|
24
|
+
* socket, which `spawnProcess`'s child-process model doesn't support, and
|
|
25
|
+
* which no ordinary application would cooperate with anyway.
|
|
26
|
+
*
|
|
27
|
+
* **Examined for a fix and deliberately not "fixed".** Holding the probe
|
|
28
|
+
* socket open for longer does not narrow the window, it widens it: the
|
|
29
|
+
* child cannot bind while we hold the port, and the dominant term is the
|
|
30
|
+
* child's own startup, not our release. Retrying on the child's bind error
|
|
31
|
+
* would mean parsing that error out of process output, which is
|
|
32
|
+
* per-language text and has no place in a language-neutral controller.
|
|
33
|
+
* What was done instead is to make the one symptom legible: `awaitReadiness`
|
|
34
|
+
* now names the endpoint it probed, so a collision reads as "nothing
|
|
35
|
+
* answered on this port" rather than as an unexplained application failure.
|
|
36
|
+
* In practice the window is a handful of milliseconds and readiness always
|
|
37
|
+
* runs afterwards, so a genuine collision fails loudly -- it is just no
|
|
38
|
+
* longer misattributed.
|
|
39
|
+
*/
|
|
40
|
+
export function allocateEphemeralPort() {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const probe = createServer();
|
|
43
|
+
probe.once("error", reject);
|
|
44
|
+
probe.listen(0, () => {
|
|
45
|
+
const address = probe.address();
|
|
46
|
+
if (address === null || typeof address === "string") {
|
|
47
|
+
probe.close();
|
|
48
|
+
reject(new Error("could not determine an ephemeral port"));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const { port } = address;
|
|
52
|
+
probe.close(() => resolve(port));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const DEFAULT_GRACE_PERIOD_MS = 5000;
|
|
57
|
+
export function spawnProcess(options) {
|
|
58
|
+
let startedAt = "";
|
|
59
|
+
let exitedAt = null;
|
|
60
|
+
let exitCode = null;
|
|
61
|
+
let signal = null;
|
|
62
|
+
let outputBuffer = "";
|
|
63
|
+
let killStarted = false;
|
|
64
|
+
let child;
|
|
65
|
+
let settled = false;
|
|
66
|
+
// Set only for the container backend (applyContainerSandbox's own
|
|
67
|
+
// returned name) -- see kill()'s own comment for why the container
|
|
68
|
+
// needs a real, separate stop mechanism from the bwrap path's group-kill.
|
|
69
|
+
let containerName = null;
|
|
70
|
+
let resolveExit;
|
|
71
|
+
let exitPromise;
|
|
72
|
+
// Everything a (re)spawn needs to reset, in one place -- `restart()` calls
|
|
73
|
+
// this again after the previous child has fully exited, and the two call
|
|
74
|
+
// sites (initial construction, restart) must not drift into resetting
|
|
75
|
+
// different subsets of this state.
|
|
76
|
+
function spawnChild() {
|
|
77
|
+
startedAt = new Date().toISOString();
|
|
78
|
+
exitedAt = null;
|
|
79
|
+
exitCode = null;
|
|
80
|
+
signal = null;
|
|
81
|
+
outputBuffer = "";
|
|
82
|
+
killStarted = false;
|
|
83
|
+
settled = false;
|
|
84
|
+
exitPromise = new Promise((resolve) => {
|
|
85
|
+
resolveExit = resolve;
|
|
86
|
+
});
|
|
87
|
+
// "container" skips applyResourceLimits entirely (see SpawnProcessOptions.
|
|
88
|
+
// sandboxBackend's own comment on why prlimit composition isn't attempted
|
|
89
|
+
// for this backend yet) -- every other case (the default, "bwrap")
|
|
90
|
+
// behaves exactly as before this field existed.
|
|
91
|
+
const wrapped = options.sandboxBackend === "container"
|
|
92
|
+
? applyContainerSandbox({
|
|
93
|
+
interpreterCommand: options.command,
|
|
94
|
+
command: options.command,
|
|
95
|
+
args: options.args ?? [],
|
|
96
|
+
cwd: options.cwd ?? process.cwd(),
|
|
97
|
+
processEnv: { ...process.env, ...options.env },
|
|
98
|
+
...(options.filesystemPolicy === undefined ? {} : { filesystemPolicy: options.filesystemPolicy }),
|
|
99
|
+
...(options.networkPolicy === undefined ? {} : { networkPolicy: options.networkPolicy }),
|
|
100
|
+
})
|
|
101
|
+
: applySandbox({
|
|
102
|
+
interpreterCommand: options.command,
|
|
103
|
+
...applyResourceLimits(options.command, options.args ?? [], options.resourceLimits),
|
|
104
|
+
cwd: options.cwd ?? process.cwd(),
|
|
105
|
+
...(options.filesystemPolicy === undefined ? {} : { filesystemPolicy: options.filesystemPolicy }),
|
|
106
|
+
...(options.networkPolicy === undefined ? {} : { networkPolicy: options.networkPolicy }),
|
|
107
|
+
});
|
|
108
|
+
containerName = wrapped.containerName ?? null;
|
|
109
|
+
child = spawn(wrapped.command, wrapped.args, {
|
|
110
|
+
cwd: options.cwd,
|
|
111
|
+
env: { ...process.env, ...options.env },
|
|
112
|
+
detached: true,
|
|
113
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
114
|
+
});
|
|
115
|
+
child.stdout?.on("data", (chunk) => {
|
|
116
|
+
outputBuffer += chunk.toString("utf8");
|
|
117
|
+
});
|
|
118
|
+
child.stderr?.on("data", (chunk) => {
|
|
119
|
+
outputBuffer += chunk.toString("utf8");
|
|
120
|
+
});
|
|
121
|
+
// Node does not call `spawn()`'s error path synchronously -- a bad
|
|
122
|
+
// command (ENOENT) surfaces later as an `'error'` event, never a thrown
|
|
123
|
+
// exception here, and per Node's own docs `'exit'` may never fire after
|
|
124
|
+
// an `'error'`. An EventEmitter's unhandled `'error'` event crashes the
|
|
125
|
+
// process, and with no `'exit'` guaranteed either, `exitPromise` would
|
|
126
|
+
// otherwise hang forever -- both `'exit'` and `'error'` settle the same
|
|
127
|
+
// promise exactly once, so a failed-to-spawn process reads as an
|
|
128
|
+
// ordinary never-became-ready process (no pid, no output, no readiness
|
|
129
|
+
// success) rather than crashing the caller or hanging its cleanup.
|
|
130
|
+
function settle(info) {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
settled = true;
|
|
134
|
+
exitedAt = new Date().toISOString();
|
|
135
|
+
exitCode = info.exitCode;
|
|
136
|
+
signal = info.signal;
|
|
137
|
+
resolveExit(info);
|
|
138
|
+
}
|
|
139
|
+
child.once("exit", (code, exitSignal) => settle({ exitCode: code, signal: exitSignal }));
|
|
140
|
+
child.once("error", () => settle({ exitCode: null, signal: null }));
|
|
141
|
+
}
|
|
142
|
+
spawnChild();
|
|
143
|
+
function handle() {
|
|
144
|
+
return {
|
|
145
|
+
processId: options.processId,
|
|
146
|
+
command: options.command,
|
|
147
|
+
pid: child.pid ?? null,
|
|
148
|
+
startedAt,
|
|
149
|
+
exitedAt,
|
|
150
|
+
exitCode,
|
|
151
|
+
signal,
|
|
152
|
+
serviceName: options.serviceName ?? null,
|
|
153
|
+
port: options.port ?? null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async function kill(gracePeriodMs = DEFAULT_GRACE_PERIOD_MS) {
|
|
157
|
+
if (killStarted || exitedAt !== null) {
|
|
158
|
+
await exitPromise;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
killStarted = true;
|
|
162
|
+
const pid = child.pid;
|
|
163
|
+
if (pid === undefined) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (containerName !== null) {
|
|
167
|
+
// Real finding from this backend's own escape tests, not assumed up
|
|
168
|
+
// front: the local `docker` CLI process is not the container -- it
|
|
169
|
+
// runs under `dockerd`, a separate process tree entirely. Signaling
|
|
170
|
+
// the CLI's process group (the mechanism below, correct for bwrap,
|
|
171
|
+
// which execs the sandboxed process directly) left a real orphaned
|
|
172
|
+
// container running in testing. `docker stop` targets the actual
|
|
173
|
+
// container by name and is the real stop mechanism for this backend;
|
|
174
|
+
// `--rm` (set by applyContainerSandbox) then removes it once stopped.
|
|
175
|
+
// Still falls through to the group-kill below afterward, as a
|
|
176
|
+
// backstop for the local CLI process itself.
|
|
177
|
+
try {
|
|
178
|
+
await execFileAsync("docker", ["stop", "--time", String(Math.max(1, Math.round(gracePeriodMs / 1000))), containerName]);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Already stopped/removed (e.g. the process exited on its own
|
|
182
|
+
// first) -- not an error worth surfacing to the caller.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
signalGroup(pid, "SIGTERM");
|
|
186
|
+
const exited = await Promise.race([exitPromise.then(() => true), sleep(gracePeriodMs).then(() => false)]);
|
|
187
|
+
if (!exited) {
|
|
188
|
+
signalGroup(pid, "SIGKILL");
|
|
189
|
+
await exitPromise;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function restart(gracePeriodMs = DEFAULT_GRACE_PERIOD_MS) {
|
|
193
|
+
// Safe whether the process is still alive, already exited on its own,
|
|
194
|
+
// or already mid-kill -- `kill()` is idempotent in all three cases and
|
|
195
|
+
// this always waits for the OLD child to be fully gone before
|
|
196
|
+
// `spawnChild()` reassigns `child`/`exitPromise`/etc., so no event from
|
|
197
|
+
// the outgoing process can land on the incoming one's state.
|
|
198
|
+
await kill(gracePeriodMs);
|
|
199
|
+
spawnChild();
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
handle,
|
|
203
|
+
readOutput: () => outputBuffer,
|
|
204
|
+
waitForExit: () => exitPromise,
|
|
205
|
+
kill,
|
|
206
|
+
restart,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* `process.kill(pid, 0)` sends no signal — it only asks the kernel whether
|
|
211
|
+
* delivery would be possible. `ESRCH` means no such pid; `EPERM` means a
|
|
212
|
+
* pid we don't own but that unambiguously exists, still alive for this
|
|
213
|
+
* purpose. Any other error is treated as "exists" rather than guessed away.
|
|
214
|
+
*
|
|
215
|
+
* A second, deliberate copy of `packages/orchestrator/src/attach-to-running-process.ts`'s
|
|
216
|
+
* function of the same name, not an import of it: `orchestrator` depends on
|
|
217
|
+
* `controller` (see `orchestrator/package.json`), so the reverse import
|
|
218
|
+
* would be circular. Tiny and pure, the same "duplicated because both
|
|
219
|
+
* copies are easy to notice drift" precedent `attach-to-running-jvm-process.ts`'s
|
|
220
|
+
* own `reconstructJvmSourcePath` already sets for exactly this reason.
|
|
221
|
+
*/
|
|
222
|
+
export function isProcessAlive(pid) {
|
|
223
|
+
try {
|
|
224
|
+
process.kill(pid, 0);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return error.code === "EPERM";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const DEFAULT_ATTACH_POLL_INTERVAL_MS = 50;
|
|
232
|
+
/**
|
|
233
|
+
* `ManagedProcess` for a service `ExecutionController.run()` attaches to
|
|
234
|
+
* instead of spawning (`ServiceConfiguration.attach`). Reads
|
|
235
|
+
* `attach.logFilePath` fresh on every `readOutput()` call — the same
|
|
236
|
+
* growing-whole-buffer contract a spawned process's in-memory pipe buffer
|
|
237
|
+
* already has (`spawnProcess`'s own `outputBuffer`), so `createPollingProcessOutputSource`
|
|
238
|
+
* (`@descryy/runtime-orchestrator`) needs no attach-specific branch to
|
|
239
|
+
* consume either one.
|
|
240
|
+
*
|
|
241
|
+
* **`kill()` is a deliberate no-op, not a stub.** Descry did not spawn this
|
|
242
|
+
* process and `ATTACH_MODE_SCOPE_DISCLOSURE` (`@descryy/runtime-contracts`)
|
|
243
|
+
* is unconditional: attach mode "never executes code in it or applies
|
|
244
|
+
* changes to it." `ExecutionController`'s cleanup path kills every managed
|
|
245
|
+
* process on failure/timeout/stop — for an attached service that would mean
|
|
246
|
+
* terminating a process this runtime does not own, silently, the exact
|
|
247
|
+
* thing the disclosure promises never happens. Ending observation (no
|
|
248
|
+
* further polling) is the honest equivalent of "stop" here; the target
|
|
249
|
+
* keeps running, as it was before Descry ever attached. `restart()` throws
|
|
250
|
+
* for the same reason: there is no command this runtime could relaunch it
|
|
251
|
+
* with.
|
|
252
|
+
*/
|
|
253
|
+
export function attachManagedProcess(options) {
|
|
254
|
+
const { pid, logFilePath } = options.attach;
|
|
255
|
+
if (!isProcessAlive(pid)) {
|
|
256
|
+
throw new Error(`attachManagedProcess: pid ${String(pid)} for "${options.processId}" is not running — attach requires ` +
|
|
257
|
+
`a target that already exists.`);
|
|
258
|
+
}
|
|
259
|
+
const startedAt = new Date().toISOString();
|
|
260
|
+
let exitedAt = null;
|
|
261
|
+
let exitPromise = null;
|
|
262
|
+
function handle() {
|
|
263
|
+
if (exitedAt === null && !isProcessAlive(pid)) {
|
|
264
|
+
exitedAt = new Date().toISOString();
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
processId: options.processId,
|
|
268
|
+
// No command to report — this runtime never launched this process,
|
|
269
|
+
// so there is nothing here that isn't a guess. Disclosed rather than
|
|
270
|
+
// fabricated.
|
|
271
|
+
command: `<attached: pid ${String(pid)}, ${logFilePath}>`,
|
|
272
|
+
pid,
|
|
273
|
+
startedAt,
|
|
274
|
+
exitedAt,
|
|
275
|
+
// Neither is observable for a process we did not spawn and do not
|
|
276
|
+
// reap — see this function's own module doc on why `kill()` never
|
|
277
|
+
// signals it, which is exactly what would let us learn either.
|
|
278
|
+
exitCode: null,
|
|
279
|
+
signal: null,
|
|
280
|
+
serviceName: options.serviceName ?? null,
|
|
281
|
+
port: options.port ?? null,
|
|
282
|
+
attached: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function readOutput() {
|
|
286
|
+
try {
|
|
287
|
+
return readFileSync(logFilePath, "utf8");
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
291
|
+
throw new Error(`attachManagedProcess: could not read log file "${logFilePath}" for pid ${String(pid)}: ${detail}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function waitForExit() {
|
|
295
|
+
exitPromise ??= new Promise((resolve) => {
|
|
296
|
+
const poll = () => {
|
|
297
|
+
if (!isProcessAlive(pid)) {
|
|
298
|
+
exitedAt ??= new Date().toISOString();
|
|
299
|
+
resolve({ exitCode: null, signal: null });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
setTimeout(poll, DEFAULT_ATTACH_POLL_INTERVAL_MS);
|
|
303
|
+
};
|
|
304
|
+
poll();
|
|
305
|
+
});
|
|
306
|
+
return exitPromise;
|
|
307
|
+
}
|
|
308
|
+
function kill() {
|
|
309
|
+
// Deliberate no-op — see this function's own module doc.
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
}
|
|
312
|
+
function restart() {
|
|
313
|
+
return Promise.reject(new Error(`attachManagedProcess: cannot restart an attached process (pid ${String(pid)}) — Descry did not spawn ` +
|
|
314
|
+
`it and has no command to relaunch it with.`));
|
|
315
|
+
}
|
|
316
|
+
return { handle, readOutput, waitForExit, kill, restart };
|
|
317
|
+
}
|
|
318
|
+
const execFileAsync = promisify(execFile);
|
|
319
|
+
function signalGroup(pid, signal) {
|
|
320
|
+
try {
|
|
321
|
+
// Negative pid targets the whole process group (the child is its
|
|
322
|
+
// leader, per `detached: true`), not just the immediate process --
|
|
323
|
+
// this is the orphan-prevention mechanism.
|
|
324
|
+
process.kill(-pid, signal);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Group already gone -- nothing to signal, not an error.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function sleep(ms) {
|
|
331
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=process-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-manager.js","sourceRoot":"","sources":["../src/process-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readiness (plan §6): "Readiness must not equal 'process exists.'" —
|
|
3
|
+
* supported strategies are HTTP health check, TCP/port check, a known log
|
|
4
|
+
* pattern, a real subprocess command, or a caller-supplied hook (which
|
|
5
|
+
* covers the plan's "framework readiness hook" and "user-defined readiness"
|
|
6
|
+
* — two names for the same shape, a caller-provided async predicate,
|
|
7
|
+
* consolidated into one mechanism rather than two near-identical ones).
|
|
8
|
+
*
|
|
9
|
+
* **`command` is not `custom-hook` (RT-193).** An earlier version of this
|
|
10
|
+
* comment folded the plan's "command check" into `custom-hook` too, on the
|
|
11
|
+
* reasoning that both are "a caller-provided check." That collapsed a real
|
|
12
|
+
* distinction: `custom-hook` runs a caller's own in-process predicate,
|
|
13
|
+
* while `command` runs an actual subprocess and reads its actual exit
|
|
14
|
+
* code — the same "declared, not inferred" shape `ServiceConfiguration`'s
|
|
15
|
+
* own `command` field already uses, one layer over. A caller with a real
|
|
16
|
+
* CLI health check (`pg_isready`, a framework's own readiness script) had
|
|
17
|
+
* no way to express that without either wrapping it in a hand-written
|
|
18
|
+
* `custom-hook` predicate that shells out itself, or reimplementing the
|
|
19
|
+
* subprocess plumbing per caller — `command` is that plumbing, built once.
|
|
20
|
+
*
|
|
21
|
+
* The plan requires recording which mechanism succeeded — `ReadinessResult`
|
|
22
|
+
* makes that a field, not something inferred after the fact.
|
|
23
|
+
*
|
|
24
|
+
* **What `ready: true` does and does not claim (RT-024).** All four
|
|
25
|
+
* mechanisms answer one question — is something listening / responding
|
|
26
|
+
* where expected — and none answer whether it is the process *this*
|
|
27
|
+
* `spawnProcess` call produced. A stale listener from an unrelated, already-
|
|
28
|
+
* dead run satisfies `http`/`tcp-port` identically to a correct one; nothing
|
|
29
|
+
* here can tell them apart. `log-pattern` only carries the stronger,
|
|
30
|
+
* this-run-specific guarantee when its `read` is wired to *this* execution's
|
|
31
|
+
* own `ManagedProcess.readOutput()` — the type does not enforce that wiring,
|
|
32
|
+
* it is a caller discipline. `custom-hook` inherits whatever the caller's
|
|
33
|
+
* predicate actually checks.
|
|
34
|
+
*
|
|
35
|
+
* Callers that bind a fixed, guessable host:port are exposed to this gap;
|
|
36
|
+
* an ephemeral port closes it by construction (nothing else can already be
|
|
37
|
+
* bound there) rather than by detection, and is the default recommendation
|
|
38
|
+
* over a startup-nonce scheme, which only earns its extra machinery when a
|
|
39
|
+
* fixed port is genuinely unavoidable (a contract with an external tool).
|
|
40
|
+
* See `descry-runtime`'s `DECISIONS.md` RT-024 for how this was found —
|
|
41
|
+
* a real 30-second collector timeout that looked like a Playwright/DOM bug
|
|
42
|
+
* and was actually a correct wait against an orphaned process from a
|
|
43
|
+
* different, already-dead session.
|
|
44
|
+
*
|
|
45
|
+
* **A single probe attempt cannot outlive `timeoutMs` (RT-030).** `http`
|
|
46
|
+
* and `tcp-port` each bind their I/O to the *remaining* time until
|
|
47
|
+
* `deadline`, not a fresh per-attempt budget — a target that accepts a
|
|
48
|
+
* connection and never responds no longer lets one probe attempt run past
|
|
49
|
+
* the caller's own deadline. Measured, not theoretical: an unbound `fetch`
|
|
50
|
+
* here let `awaitReadiness` run past its own `timeoutMs` (>8s observed
|
|
51
|
+
* against a 3s budget). `command` honors the same discipline, the same
|
|
52
|
+
* way: `execFile`'s own `timeout` option is set to the remaining time until
|
|
53
|
+
* `deadline`, so a command that hangs instead of exiting is killed rather
|
|
54
|
+
* than left to run past the caller's own budget — it owns a real OS
|
|
55
|
+
* resource (a child process) exactly like the http/tcp-port sockets do, so
|
|
56
|
+
* it gets the same treatment. `custom-hook` is not wrapped the same way — a
|
|
57
|
+
* caller-supplied predicate owns its own timeout behavior, same as every
|
|
58
|
+
* other `Collector`-shaped callback contract in this repo (`collector.ts`'s
|
|
59
|
+
* own docs put "must resolve, not hang" on the implementer); imposing a
|
|
60
|
+
* surprise external cancellation on arbitrary caller code would be a
|
|
61
|
+
* different, larger decision than fixing the mechanisms that own a real OS
|
|
62
|
+
* resource this module can actually clean up.
|
|
63
|
+
*/
|
|
64
|
+
export declare const READINESS_MECHANISMS: readonly ["http", "tcp-port", "log-pattern", "command", "custom-hook"];
|
|
65
|
+
export type ReadinessMechanism = (typeof READINESS_MECHANISMS)[number];
|
|
66
|
+
export interface HttpReadinessCheck {
|
|
67
|
+
readonly kind: "http";
|
|
68
|
+
readonly url: string;
|
|
69
|
+
readonly expectedStatus?: number;
|
|
70
|
+
}
|
|
71
|
+
export interface TcpPortReadinessCheck {
|
|
72
|
+
readonly kind: "tcp-port";
|
|
73
|
+
readonly host: string;
|
|
74
|
+
readonly port: number;
|
|
75
|
+
}
|
|
76
|
+
export interface LogPatternReadinessCheck {
|
|
77
|
+
readonly kind: "log-pattern";
|
|
78
|
+
/**
|
|
79
|
+
* Tested against `read()`'s full return value on every poll (RT-032's
|
|
80
|
+
* `awaitReadiness` loop). `read` is documented to wire to
|
|
81
|
+
* `ManagedProcess.readOutput()`, which returns the entire accumulated
|
|
82
|
+
* stdout+stderr buffer with **no size cap** — a caller-supplied pattern
|
|
83
|
+
* with an unbounded quantifier immediately before a literal that can be
|
|
84
|
+
* absent (the exact shape RT-069 found and fixed in
|
|
85
|
+
* `evidence-store/redaction.ts`'s `EMAIL_PATTERN`, and RT-070 also found
|
|
86
|
+
* and fixed in this package's own `PORT_PLACEHOLDER`) would run that
|
|
87
|
+
* catastrophic-backtracking cost against a growing buffer on every single
|
|
88
|
+
* poll. Nothing in this repo currently supplies such a pattern here, so
|
|
89
|
+
* this is a disclosed risk at the call site rather than a fixed one — the
|
|
90
|
+
* regex is the caller's, not this module's, to bound.
|
|
91
|
+
*/
|
|
92
|
+
readonly pattern: RegExp;
|
|
93
|
+
/** Pulls currently-buffered output — wire this to a ManagedProcess's `readOutput`. */
|
|
94
|
+
readonly read: () => string;
|
|
95
|
+
}
|
|
96
|
+
export interface CommandReadinessCheck {
|
|
97
|
+
readonly kind: "command";
|
|
98
|
+
readonly command: string;
|
|
99
|
+
readonly args?: readonly string[];
|
|
100
|
+
/** Defaults to this process's own `cwd` (`execFile`'s own default) when omitted. */
|
|
101
|
+
readonly cwd?: string;
|
|
102
|
+
}
|
|
103
|
+
export interface CustomHookReadinessCheck {
|
|
104
|
+
readonly kind: "custom-hook";
|
|
105
|
+
readonly check: () => Promise<boolean>;
|
|
106
|
+
}
|
|
107
|
+
export type ReadinessCheck = HttpReadinessCheck | TcpPortReadinessCheck | LogPatternReadinessCheck | CommandReadinessCheck | CustomHookReadinessCheck;
|
|
108
|
+
export interface ReadinessResult {
|
|
109
|
+
readonly ready: boolean;
|
|
110
|
+
/** Which mechanism succeeded — null when none did within the timeout. */
|
|
111
|
+
readonly mechanism: ReadinessMechanism | null;
|
|
112
|
+
readonly elapsedMs: number;
|
|
113
|
+
readonly reason: string | null;
|
|
114
|
+
}
|
|
115
|
+
export interface AwaitReadinessOptions {
|
|
116
|
+
readonly timeoutMs: number;
|
|
117
|
+
readonly pollIntervalMs?: number;
|
|
118
|
+
}
|
|
119
|
+
export declare function awaitReadiness(checks: readonly ReadinessCheck[], options: AwaitReadinessOptions): Promise<ReadinessResult>;
|
|
120
|
+
//# sourceMappingURL=readiness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../src/readiness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAKH,eAAO,MAAM,oBAAoB,wEAAyE,CAAC;AAC3G,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,MAAM,cAAc,GACtB,kBAAkB,GAClB,qBAAqB,GACrB,wBAAwB,GACxB,qBAAqB,GACrB,wBAAwB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAwC1B"}
|