@descryy/runtime-orchestrator 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/attach-to-running-jvm-process.d.ts +139 -0
- package/dist/attach-to-running-jvm-process.d.ts.map +1 -0
- package/dist/attach-to-running-jvm-process.js +338 -0
- package/dist/attach-to-running-jvm-process.js.map +1 -0
- package/dist/attach-to-running-node-process.d.ts +138 -0
- package/dist/attach-to-running-node-process.d.ts.map +1 -0
- package/dist/attach-to-running-node-process.js +315 -0
- package/dist/attach-to-running-node-process.js.map +1 -0
- package/dist/attach-to-running-process.d.ts +70 -0
- package/dist/attach-to-running-process.d.ts.map +1 -0
- package/dist/attach-to-running-process.js +90 -0
- package/dist/attach-to-running-process.js.map +1 -0
- package/dist/cdp-client.d.ts +37 -0
- package/dist/cdp-client.d.ts.map +1 -0
- package/dist/cdp-client.js +111 -0
- package/dist/cdp-client.js.map +1 -0
- package/dist/cgroup-partial-restriction.d.ts +127 -0
- package/dist/cgroup-partial-restriction.d.ts.map +1 -0
- package/dist/cgroup-partial-restriction.js +236 -0
- package/dist/cgroup-partial-restriction.js.map +1 -0
- package/dist/collector-version.d.ts +12 -0
- package/dist/collector-version.d.ts.map +1 -0
- package/dist/collector-version.js +14 -0
- package/dist/collector-version.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumented-execution.d.ts +111 -0
- package/dist/instrumented-execution.d.ts.map +1 -0
- package/dist/instrumented-execution.js +363 -0
- package/dist/instrumented-execution.js.map +1 -0
- package/dist/jvm-agent/build.d.ts +72 -0
- package/dist/jvm-agent/build.d.ts.map +1 -0
- package/dist/jvm-agent/build.js +156 -0
- package/dist/jvm-agent/build.js.map +1 -0
- package/dist/polling-output-source.d.ts +28 -0
- package/dist/polling-output-source.d.ts.map +1 -0
- package/dist/polling-output-source.js +73 -0
- package/dist/polling-output-source.js.map +1 -0
- package/dist/profile-backend-observation.d.ts +94 -0
- package/dist/profile-backend-observation.d.ts.map +1 -0
- package/dist/profile-backend-observation.js +136 -0
- package/dist/profile-backend-observation.js.map +1 -0
- package/dist/respawn-and-supervise.d.ts +119 -0
- package/dist/respawn-and-supervise.d.ts.map +1 -0
- package/dist/respawn-and-supervise.js +212 -0
- package/dist/respawn-and-supervise.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-client.js","sourceRoot":"","sources":["../src/cdp-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partial going-forward resource restriction for an already-running,
|
|
3
|
+
* attached-to process, via cgroups v2 -- the second, narrower alternative
|
|
4
|
+
* next to respawn-and-supervise (`respawn-and-supervise.ts`).
|
|
5
|
+
*
|
|
6
|
+
* **What this genuinely gets you, stated exactly, because the honest
|
|
7
|
+
* boundary here is not the same shape as spawn-mode's.** Moving a live
|
|
8
|
+
* pid's cgroup membership (writing it into a delegated cgroup's own
|
|
9
|
+
* `cgroup.procs`, per the kernel's cgroups v2 unified hierarchy) applies
|
|
10
|
+
* real, kernel-enforced resource limits -- `memory.max`, `cpu.max`,
|
|
11
|
+
* `pids.max` -- to a process this runtime never spawned, without killing
|
|
12
|
+
* or restarting it. That is real and this module proves it for real (see
|
|
13
|
+
* the test suite). **It does NOT, and structurally cannot, give the moved
|
|
14
|
+
* process any filesystem or network isolation.** Changing which mount or
|
|
15
|
+
* network namespace an already-running process belongs to needs `setns()`
|
|
16
|
+
* against a live pid -- a fundamentally different, far more invasive
|
|
17
|
+
* operation than a cgroup move, not attempted here and out of scope for
|
|
18
|
+
* this pass. Every value this module returns, and every place this
|
|
19
|
+
* capability is described, says so: `CGROUP_PARTIAL_RESTRICTION_SCOPE`
|
|
20
|
+
* below is the literal, load-bearing string every result carries, precisely
|
|
21
|
+
* so nothing downstream can round it up to "sandboxed" or "isolated" --
|
|
22
|
+
* words that would misrepresent this as equivalent to `sandbox.ts`'s real
|
|
23
|
+
* spawn-time bwrap isolation.
|
|
24
|
+
*
|
|
25
|
+
* **How the delegated cgroup is found -- measured on this host, not
|
|
26
|
+
* assumed from cgroups v2 documentation.** A non-root user cannot write
|
|
27
|
+
* anywhere under `/sys/fs/cgroup` by default; what makes any of this
|
|
28
|
+
* possible at all is a systemd user session's own delegation
|
|
29
|
+
* (`user@<uid>.service`, `Delegate=yes`), which typically extends through
|
|
30
|
+
* `app.slice`/`session.slice`/`background.slice` beneath it. But
|
|
31
|
+
* delegation is not uniform all the way down: cgroups v2's "no internal
|
|
32
|
+
* processes" rule means a *leaf* cgroup that already directly holds a
|
|
33
|
+
* process (a systemd `.scope`, including the very one this Node process
|
|
34
|
+
* itself runs in) generally cannot have its `subtree_control` extended
|
|
35
|
+
* further -- writing to it fails with `EBUSY`, measured directly against
|
|
36
|
+
* this repo's own dev host while building this module. `findDelegatedCgroupAncestor`
|
|
37
|
+
* therefore does not trust `cgroup.subtree_control` text or permission
|
|
38
|
+
* bits alone; it walks up from this process's own `/proc/self/cgroup` path
|
|
39
|
+
* and, at each ancestor, creates a real, temporary probe child directory
|
|
40
|
+
* and checks whether the kernel actually gave it a `memory.max` file --
|
|
41
|
+
* the only way to honestly know a given ancestor's delegation reaches that
|
|
42
|
+
* far, empirically, the same "prove it against the real mechanism" standard
|
|
43
|
+
* `sandbox.ts`'s own escape tests hold spawn-mode isolation to.
|
|
44
|
+
*/
|
|
45
|
+
import type { CapabilityStatus } from "@descryy/runtime-contracts";
|
|
46
|
+
/**
|
|
47
|
+
* The literal, load-bearing scope string. Every `CgroupMoveResult` carries
|
|
48
|
+
* this exact value under `scope` -- never restated by hand elsewhere, so a
|
|
49
|
+
* caller (or a future capability-report string, or a decisions-inbox entry)
|
|
50
|
+
* that quotes `.scope` can never accidentally drift from this wording.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CGROUP_PARTIAL_RESTRICTION_SCOPE: "partial -- resource limits only, no filesystem or network isolation";
|
|
53
|
+
/**
|
|
54
|
+
* Walks up from this process's own cgroup toward `/sys/fs/cgroup`, returning
|
|
55
|
+
* the deepest (most specific) ancestor directory whose children genuinely
|
|
56
|
+
* get a real `memory.max` -- i.e. the actual delegation boundary, not
|
|
57
|
+
* merely a directory this uid happens to own. `null` when cgroups v2 is not
|
|
58
|
+
* mounted, this process's own cgroup path cannot be determined, or no
|
|
59
|
+
* ancestor all the way to the root passes the probe (no delegation at all).
|
|
60
|
+
*/
|
|
61
|
+
export declare function findDelegatedCgroupAncestor(): string | null;
|
|
62
|
+
export declare function cgroupV2Capability(): CapabilityStatus;
|
|
63
|
+
export interface CgroupResourceLimits {
|
|
64
|
+
/**
|
|
65
|
+
* Maps to cgroups v2 `memory.max`, in bytes. **Also writes `memory.swap.max=0`
|
|
66
|
+
* for the same cgroup, deliberately, when this is set** -- measured directly
|
|
67
|
+
* while building this module: with swap available on the host (the common
|
|
68
|
+
* case), `memory.max` alone does not kill anything, it just makes the
|
|
69
|
+
* kernel push the cgroup's pages to swap under pressure, so a caller
|
|
70
|
+
* declaring a memory cap would silently get "slows down and swaps"
|
|
71
|
+
* instead of the real, hard boundary the field name promises. Disabling
|
|
72
|
+
* swap for the created cgroup is what makes `memory.max` an actual
|
|
73
|
+
* enforced limit (the kernel's own cgroup-aware OOM killer terminates a
|
|
74
|
+
* process here, SIGKILL, once reclaim genuinely cannot bring usage under
|
|
75
|
+
* the cap) rather than a soft one. Not independently toggleable in this
|
|
76
|
+
* pass -- if a future caller genuinely wants "cap it but let it swap,"
|
|
77
|
+
* that is a real, separate field to add later, not silently assumed here.
|
|
78
|
+
*/
|
|
79
|
+
readonly memoryMaxBytes?: number;
|
|
80
|
+
/** Maps to cgroups v2 `pids.max` -- caps the number of tasks (processes/threads) this cgroup may hold at once. */
|
|
81
|
+
readonly maxProcesses?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Raw cgroups v2 `cpu.max` value (`"$MAX $PERIOD"`, e.g. `"50000 100000"`
|
|
84
|
+
* for 50% of one CPU). Declaring this when the delegated ancestor does
|
|
85
|
+
* not actually expose a writable `cpu.max` to its children is a refusal
|
|
86
|
+
* (throws), not a silently-dropped limit -- matching `applySandbox`'s own
|
|
87
|
+
* "declared, refused rather than silently ignored if unenforceable"
|
|
88
|
+
* precedent. Measured on this repo's own dev host: `cpu` was NOT
|
|
89
|
+
* delegated at the same level `memory`/`pids` were, so this is a real,
|
|
90
|
+
* not hypothetical, gap a caller can hit.
|
|
91
|
+
*/
|
|
92
|
+
readonly cpuMax?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface CgroupMoveResult {
|
|
95
|
+
readonly applied: true;
|
|
96
|
+
readonly cgroupPath: string;
|
|
97
|
+
/** Always the literal `CGROUP_PARTIAL_RESTRICTION_SCOPE` string -- see its own doc for why this is never restated by hand. */
|
|
98
|
+
readonly scope: typeof CGROUP_PARTIAL_RESTRICTION_SCOPE;
|
|
99
|
+
readonly appliedLimits: CgroupResourceLimits;
|
|
100
|
+
/**
|
|
101
|
+
* Best-effort removal of the cgroup this call created. Succeeds only once
|
|
102
|
+
* `cgroup.procs` is empty -- the pid exited, or was itself moved
|
|
103
|
+
* elsewhere -- because cgroups v2 refuses to remove a non-empty cgroup,
|
|
104
|
+
* the same "no internal processes hiding a live one" rule
|
|
105
|
+
* `findDelegatedCgroupAncestor`'s own probe relies on. Returns whether
|
|
106
|
+
* removal actually happened; never throws.
|
|
107
|
+
*/
|
|
108
|
+
readonly release: () => boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Moves `options.pid` -- an already-running process this runtime did not
|
|
112
|
+
* spawn -- into a newly created, resource-limited cgroup. Throws (does not
|
|
113
|
+
* silently proceed) when the target is not alive, when `cgroupV2Capability()`
|
|
114
|
+
* reports unavailable, or when a declared limit's controller file does not
|
|
115
|
+
* actually exist under the delegated ancestor found for it.
|
|
116
|
+
*
|
|
117
|
+
* Limits are written to the new cgroup BEFORE the pid is moved into it, so
|
|
118
|
+
* a process is never observed inside a cgroup with a half-applied limit
|
|
119
|
+
* set; if any write fails, the created (still-empty) cgroup is removed and
|
|
120
|
+
* the error is rethrown rather than left as an orphaned, unlimited group.
|
|
121
|
+
*/
|
|
122
|
+
export declare function moveProcessIntoRestrictedCgroup(options: {
|
|
123
|
+
readonly pid: number;
|
|
124
|
+
readonly limits: CgroupResourceLimits;
|
|
125
|
+
readonly nameHint?: string;
|
|
126
|
+
}): CgroupMoveResult;
|
|
127
|
+
//# sourceMappingURL=cgroup-partial-restriction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cgroup-partial-restriction.d.ts","sourceRoot":"","sources":["../src/cgroup-partial-restriction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAKnE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,EAAG,qEAA8E,CAAC;AAkD/H;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,GAAG,IAAI,CAiB3D;AAED,wBAAgB,kBAAkB,IAAI,gBAAgB,CAkBrD;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,kHAAkH;IAClH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,8HAA8H;IAC9H,QAAQ,CAAC,KAAK,EAAE,OAAO,gCAAgC,CAAC;IACxD,QAAQ,CAAC,aAAa,EAAE,oBAAoB,CAAC;IAC7C;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACvD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,GAAG,gBAAgB,CA2EnB"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partial going-forward resource restriction for an already-running,
|
|
3
|
+
* attached-to process, via cgroups v2 -- the second, narrower alternative
|
|
4
|
+
* next to respawn-and-supervise (`respawn-and-supervise.ts`).
|
|
5
|
+
*
|
|
6
|
+
* **What this genuinely gets you, stated exactly, because the honest
|
|
7
|
+
* boundary here is not the same shape as spawn-mode's.** Moving a live
|
|
8
|
+
* pid's cgroup membership (writing it into a delegated cgroup's own
|
|
9
|
+
* `cgroup.procs`, per the kernel's cgroups v2 unified hierarchy) applies
|
|
10
|
+
* real, kernel-enforced resource limits -- `memory.max`, `cpu.max`,
|
|
11
|
+
* `pids.max` -- to a process this runtime never spawned, without killing
|
|
12
|
+
* or restarting it. That is real and this module proves it for real (see
|
|
13
|
+
* the test suite). **It does NOT, and structurally cannot, give the moved
|
|
14
|
+
* process any filesystem or network isolation.** Changing which mount or
|
|
15
|
+
* network namespace an already-running process belongs to needs `setns()`
|
|
16
|
+
* against a live pid -- a fundamentally different, far more invasive
|
|
17
|
+
* operation than a cgroup move, not attempted here and out of scope for
|
|
18
|
+
* this pass. Every value this module returns, and every place this
|
|
19
|
+
* capability is described, says so: `CGROUP_PARTIAL_RESTRICTION_SCOPE`
|
|
20
|
+
* below is the literal, load-bearing string every result carries, precisely
|
|
21
|
+
* so nothing downstream can round it up to "sandboxed" or "isolated" --
|
|
22
|
+
* words that would misrepresent this as equivalent to `sandbox.ts`'s real
|
|
23
|
+
* spawn-time bwrap isolation.
|
|
24
|
+
*
|
|
25
|
+
* **How the delegated cgroup is found -- measured on this host, not
|
|
26
|
+
* assumed from cgroups v2 documentation.** A non-root user cannot write
|
|
27
|
+
* anywhere under `/sys/fs/cgroup` by default; what makes any of this
|
|
28
|
+
* possible at all is a systemd user session's own delegation
|
|
29
|
+
* (`user@<uid>.service`, `Delegate=yes`), which typically extends through
|
|
30
|
+
* `app.slice`/`session.slice`/`background.slice` beneath it. But
|
|
31
|
+
* delegation is not uniform all the way down: cgroups v2's "no internal
|
|
32
|
+
* processes" rule means a *leaf* cgroup that already directly holds a
|
|
33
|
+
* process (a systemd `.scope`, including the very one this Node process
|
|
34
|
+
* itself runs in) generally cannot have its `subtree_control` extended
|
|
35
|
+
* further -- writing to it fails with `EBUSY`, measured directly against
|
|
36
|
+
* this repo's own dev host while building this module. `findDelegatedCgroupAncestor`
|
|
37
|
+
* therefore does not trust `cgroup.subtree_control` text or permission
|
|
38
|
+
* bits alone; it walks up from this process's own `/proc/self/cgroup` path
|
|
39
|
+
* and, at each ancestor, creates a real, temporary probe child directory
|
|
40
|
+
* and checks whether the kernel actually gave it a `memory.max` file --
|
|
41
|
+
* the only way to honestly know a given ancestor's delegation reaches that
|
|
42
|
+
* far, empirically, the same "prove it against the real mechanism" standard
|
|
43
|
+
* `sandbox.ts`'s own escape tests hold spawn-mode isolation to.
|
|
44
|
+
*/
|
|
45
|
+
import { existsSync, mkdirSync, readFileSync, rmdirSync, writeFileSync } from "node:fs";
|
|
46
|
+
import { dirname, join } from "node:path";
|
|
47
|
+
import { isProcessAlive } from "./attach-to-running-process.js";
|
|
48
|
+
const CGROUP_ROOT = "/sys/fs/cgroup";
|
|
49
|
+
/**
|
|
50
|
+
* The literal, load-bearing scope string. Every `CgroupMoveResult` carries
|
|
51
|
+
* this exact value under `scope` -- never restated by hand elsewhere, so a
|
|
52
|
+
* caller (or a future capability-report string, or a decisions-inbox entry)
|
|
53
|
+
* that quotes `.scope` can never accidentally drift from this wording.
|
|
54
|
+
*/
|
|
55
|
+
export const CGROUP_PARTIAL_RESTRICTION_SCOPE = "partial -- resource limits only, no filesystem or network isolation";
|
|
56
|
+
function ownUnifiedCgroupPath() {
|
|
57
|
+
let content;
|
|
58
|
+
try {
|
|
59
|
+
content = readFileSync("/proc/self/cgroup", "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
for (const line of content.trim().split("\n")) {
|
|
65
|
+
// cgroups v2 unified-hierarchy lines read "0::<path>" -- hierarchy id 0,
|
|
66
|
+
// no named controllers (that's a v1 concept), then the path. A host
|
|
67
|
+
// still running v1 controllers alongside v2 has other numbered lines
|
|
68
|
+
// here too; only the "0::" one is this module's concern.
|
|
69
|
+
const firstColon = line.indexOf(":");
|
|
70
|
+
const secondColon = line.indexOf(":", firstColon + 1);
|
|
71
|
+
if (firstColon === -1 || secondColon === -1)
|
|
72
|
+
continue;
|
|
73
|
+
if (line.slice(0, firstColon) === "0" && firstColon + 1 === secondColon) {
|
|
74
|
+
return line.slice(secondColon + 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Real, empirical probe -- not a permission-bit read. See this module's own
|
|
81
|
+
* doc for why: a directory can be `mkdir`-able yet still produce a child
|
|
82
|
+
* with no `memory.max` at all, when the controller was never actually
|
|
83
|
+
* enabled in that parent's own `cgroup.subtree_control` (this process's own
|
|
84
|
+
* leaf scope is exactly this shape, measured directly). The only honest
|
|
85
|
+
* test is creating a real probe child and checking what the kernel actually
|
|
86
|
+
* gave it.
|
|
87
|
+
*/
|
|
88
|
+
function probeControllableChild(candidateDir) {
|
|
89
|
+
const probe = join(candidateDir, `.descry-cgroup-probe-${String(process.pid)}-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`);
|
|
90
|
+
try {
|
|
91
|
+
mkdirSync(probe);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const controllable = existsSync(join(probe, "memory.max"));
|
|
97
|
+
try {
|
|
98
|
+
rmdirSync(probe);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Best-effort cleanup of our own just-created probe directory -- not
|
|
102
|
+
// this module's problem if something else raced to touch it first.
|
|
103
|
+
}
|
|
104
|
+
return controllable;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Walks up from this process's own cgroup toward `/sys/fs/cgroup`, returning
|
|
108
|
+
* the deepest (most specific) ancestor directory whose children genuinely
|
|
109
|
+
* get a real `memory.max` -- i.e. the actual delegation boundary, not
|
|
110
|
+
* merely a directory this uid happens to own. `null` when cgroups v2 is not
|
|
111
|
+
* mounted, this process's own cgroup path cannot be determined, or no
|
|
112
|
+
* ancestor all the way to the root passes the probe (no delegation at all).
|
|
113
|
+
*/
|
|
114
|
+
export function findDelegatedCgroupAncestor() {
|
|
115
|
+
if (process.platform !== "linux")
|
|
116
|
+
return null;
|
|
117
|
+
if (!existsSync(join(CGROUP_ROOT, "cgroup.controllers")))
|
|
118
|
+
return null;
|
|
119
|
+
const ownPath = ownUnifiedCgroupPath();
|
|
120
|
+
if (ownPath === null)
|
|
121
|
+
return null;
|
|
122
|
+
let dir = join(CGROUP_ROOT, ownPath.replace(/^\/+/, ""));
|
|
123
|
+
// Walk strictly upward, never testing CGROUP_ROOT itself -- writing
|
|
124
|
+
// limits at the true root would apply host-wide and is root-only on
|
|
125
|
+
// every real system anyway, so it is deliberately excluded rather than
|
|
126
|
+
// reached as a fallback.
|
|
127
|
+
while (dir !== CGROUP_ROOT && dirname(dir) !== dir) {
|
|
128
|
+
if (existsSync(dir) && probeControllableChild(dir))
|
|
129
|
+
return dir;
|
|
130
|
+
dir = dirname(dir);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
export function cgroupV2Capability() {
|
|
135
|
+
if (process.platform !== "linux") {
|
|
136
|
+
return { availability: "unavailable", reason: `cgroups v2 resource-limit move is Linux-only; not available on "${process.platform}"` };
|
|
137
|
+
}
|
|
138
|
+
if (!existsSync(join(CGROUP_ROOT, "cgroup.controllers"))) {
|
|
139
|
+
return { availability: "unavailable", reason: `${CGROUP_ROOT} has no cgroup.controllers file -- this host is not using the cgroups v2 unified hierarchy` };
|
|
140
|
+
}
|
|
141
|
+
const ancestor = findDelegatedCgroupAncestor();
|
|
142
|
+
if (ancestor === null) {
|
|
143
|
+
return {
|
|
144
|
+
availability: "unavailable",
|
|
145
|
+
reason: "cgroups v2 is mounted, but no writable, controller-delegated cgroup ancestor was found for this process -- commonly means this " +
|
|
146
|
+
"process is not running under a systemd user session with delegation (Delegate=yes on user@<uid>.service), or is running as a " +
|
|
147
|
+
"user with no cgroup delegation anywhere in its own ancestry. Refusing rather than silently doing nothing.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { availability: "available", reason: null };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Moves `options.pid` -- an already-running process this runtime did not
|
|
154
|
+
* spawn -- into a newly created, resource-limited cgroup. Throws (does not
|
|
155
|
+
* silently proceed) when the target is not alive, when `cgroupV2Capability()`
|
|
156
|
+
* reports unavailable, or when a declared limit's controller file does not
|
|
157
|
+
* actually exist under the delegated ancestor found for it.
|
|
158
|
+
*
|
|
159
|
+
* Limits are written to the new cgroup BEFORE the pid is moved into it, so
|
|
160
|
+
* a process is never observed inside a cgroup with a half-applied limit
|
|
161
|
+
* set; if any write fails, the created (still-empty) cgroup is removed and
|
|
162
|
+
* the error is rethrown rather than left as an orphaned, unlimited group.
|
|
163
|
+
*/
|
|
164
|
+
export function moveProcessIntoRestrictedCgroup(options) {
|
|
165
|
+
if (!isProcessAlive(options.pid)) {
|
|
166
|
+
throw new Error(`moveProcessIntoRestrictedCgroup: pid ${String(options.pid)} is not running -- cannot move a target that does not exist.`);
|
|
167
|
+
}
|
|
168
|
+
const capability = cgroupV2Capability();
|
|
169
|
+
if (capability.availability !== "available") {
|
|
170
|
+
throw new Error(`moveProcessIntoRestrictedCgroup: cannot apply -- ${capability.reason}`);
|
|
171
|
+
}
|
|
172
|
+
const ancestor = findDelegatedCgroupAncestor();
|
|
173
|
+
if (ancestor === null) {
|
|
174
|
+
// Unreachable given capability.availability === "available" above, but
|
|
175
|
+
// narrows the type honestly rather than a non-null assertion.
|
|
176
|
+
throw new Error("moveProcessIntoRestrictedCgroup: no delegated cgroup ancestor found despite capability reporting available -- internal inconsistency.");
|
|
177
|
+
}
|
|
178
|
+
const cgroupPath = join(ancestor, `descry-restrict-${options.nameHint ?? String(options.pid)}-${String(Date.now())}`);
|
|
179
|
+
mkdirSync(cgroupPath);
|
|
180
|
+
try {
|
|
181
|
+
if (options.limits.cpuMax !== undefined && !existsSync(join(cgroupPath, "cpu.max"))) {
|
|
182
|
+
throw new Error(`cpuMax was requested but the delegated cgroup ancestor (${ancestor}) does not expose a writable cpu.max to its children on this host -- ` +
|
|
183
|
+
"refusing rather than silently ignoring the limit.");
|
|
184
|
+
}
|
|
185
|
+
if (options.limits.memoryMaxBytes !== undefined) {
|
|
186
|
+
writeFileSync(join(cgroupPath, "memory.max"), String(options.limits.memoryMaxBytes));
|
|
187
|
+
// See CgroupResourceLimits.memoryMaxBytes's own comment: without this,
|
|
188
|
+
// a host with swap available reclaims into swap instead of killing,
|
|
189
|
+
// and memory.max stops being a real limit. Best-effort -- a host with
|
|
190
|
+
// no memory.swap.max file at all (swap accounting compiled out, or no
|
|
191
|
+
// swap controller delegated) still gets a real memory.max, just
|
|
192
|
+
// without this hardening; not a reason to refuse the whole call.
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync(join(cgroupPath, "memory.swap.max"), "0");
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Disclosed above, not silently pretended to have succeeded.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (options.limits.maxProcesses !== undefined) {
|
|
201
|
+
writeFileSync(join(cgroupPath, "pids.max"), String(options.limits.maxProcesses));
|
|
202
|
+
}
|
|
203
|
+
if (options.limits.cpuMax !== undefined) {
|
|
204
|
+
writeFileSync(join(cgroupPath, "cpu.max"), options.limits.cpuMax);
|
|
205
|
+
}
|
|
206
|
+
// Moved last, only once every requested limit is confirmed written.
|
|
207
|
+
writeFileSync(join(cgroupPath, "cgroup.procs"), String(options.pid));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
try {
|
|
211
|
+
rmdirSync(cgroupPath);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Best-effort -- if the pid was already moved in before the failure,
|
|
215
|
+
// the directory is non-empty and cannot be removed; nothing more this
|
|
216
|
+
// catch can honestly do about that.
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`moveProcessIntoRestrictedCgroup: failed to apply limits or move pid ${String(options.pid)} into ${cgroupPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
applied: true,
|
|
222
|
+
cgroupPath,
|
|
223
|
+
scope: CGROUP_PARTIAL_RESTRICTION_SCOPE,
|
|
224
|
+
appliedLimits: options.limits,
|
|
225
|
+
release: () => {
|
|
226
|
+
try {
|
|
227
|
+
rmdirSync(cgroupPath);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=cgroup-partial-restriction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cgroup-partial-restriction.js","sourceRoot":"","sources":["../src/cgroup-partial-restriction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Evidence.collectorVersion`'s source for evidence the orchestrator itself
|
|
3
|
+
* emits directly (e.g. `COLLECTOR_ERROR` for a collector that never got
|
|
4
|
+
* past `start()`, so no collector-owned code ever ran to attribute the row
|
|
5
|
+
* to): this package's own `package.json` "version", read from the artifact
|
|
6
|
+
* itself rather than hand-duplicated into a second string that can drift
|
|
7
|
+
* from what actually shipped. Same idea as `nodeVersion` elsewhere in this
|
|
8
|
+
* repo (`process.version`, always known, never probed) applied to a
|
|
9
|
+
* package instead of the Node runtime.
|
|
10
|
+
*/
|
|
11
|
+
export declare const COLLECTOR_VERSION: string;
|
|
12
|
+
//# sourceMappingURL=collector-version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector-version.d.ts","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,eAAO,MAAM,iBAAiB,EAAE,MAA6E,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Evidence.collectorVersion`'s source for evidence the orchestrator itself
|
|
3
|
+
* emits directly (e.g. `COLLECTOR_ERROR` for a collector that never got
|
|
4
|
+
* past `start()`, so no collector-owned code ever ran to attribute the row
|
|
5
|
+
* to): this package's own `package.json` "version", read from the artifact
|
|
6
|
+
* itself rather than hand-duplicated into a second string that can drift
|
|
7
|
+
* from what actually shipped. Same idea as `nodeVersion` elsewhere in this
|
|
8
|
+
* repo (`process.version`, always known, never probed) applied to a
|
|
9
|
+
* package instead of the Node runtime.
|
|
10
|
+
*/
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
export const COLLECTOR_VERSION = require("../package.json").version;
|
|
14
|
+
//# sourceMappingURL=collector-version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector-version.js","sourceRoot":"","sources":["../src/collector-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,CAAC,MAAM,iBAAiB,GAAY,OAAO,CAAC,iBAAiB,CAAkC,CAAC,OAAO,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { PollingProcessOutputSourceOptions } from "./polling-output-source.ts";
|
|
2
|
+
export { createPollingProcessOutputSource } from "./polling-output-source.ts";
|
|
3
|
+
export type { AttachToRunningProcessOptions } from "./attach-to-running-process.ts";
|
|
4
|
+
export { attachToRunningProcess, isProcessAlive } from "./attach-to-running-process.ts";
|
|
5
|
+
export type { AttachToRunningNodeProcessOptions } from "./attach-to-running-node-process.ts";
|
|
6
|
+
export { attachToRunningNodeProcess, discoverNodeInspectorUrl } from "./attach-to-running-node-process.ts";
|
|
7
|
+
export type { CdpSession } from "./cdp-client.ts";
|
|
8
|
+
export { connectCdp } from "./cdp-client.ts";
|
|
9
|
+
export type { AttachToRunningJvmProcessOptions } from "./attach-to-running-jvm-process.ts";
|
|
10
|
+
export { attachToRunningJvmProcess } from "./attach-to-running-jvm-process.ts";
|
|
11
|
+
export type { RespawnCommand, RespawnCommandSource, RespawnAndSuperviseOptions, RespawnAndSuperviseResult, } from "./respawn-and-supervise.ts";
|
|
12
|
+
export { respawnAndSupervise, inferRespawnCommandFromProc } from "./respawn-and-supervise.ts";
|
|
13
|
+
export type { CgroupResourceLimits, CgroupMoveResult, } from "./cgroup-partial-restriction.ts";
|
|
14
|
+
export { CGROUP_PARTIAL_RESTRICTION_SCOPE, cgroupV2Capability, findDelegatedCgroupAncestor, moveProcessIntoRestrictedCgroup, } from "./cgroup-partial-restriction.ts";
|
|
15
|
+
export type { InstrumentedExecutionInput, InstrumentedExecutionResult } from "./instrumented-execution.ts";
|
|
16
|
+
export { runInstrumentedExecution, createSourceRootResolver } from "./instrumented-execution.ts";
|
|
17
|
+
export type { BackendObservationMechanism, RunRemoteLogObservationInput, RunRemoteLogObservationResult, } from "./profile-backend-observation.ts";
|
|
18
|
+
export { backendObservationMechanismFor, runRemoteLogObservation } from "./profile-backend-observation.ts";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAC;AACpF,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,YAAY,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACxF,YAAY,EAAE,iCAAiC,EAAE,MAAM,qCAAqC,CAAC;AAC7F,OAAO,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAC3G,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC;AAC3F,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAC9F,YAAY,EACV,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC3G,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACjG,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,8BAA8B,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createPollingProcessOutputSource } from "./polling-output-source.js";
|
|
2
|
+
export { attachToRunningProcess, isProcessAlive } from "./attach-to-running-process.js";
|
|
3
|
+
export { attachToRunningNodeProcess, discoverNodeInspectorUrl } from "./attach-to-running-node-process.js";
|
|
4
|
+
export { connectCdp } from "./cdp-client.js";
|
|
5
|
+
export { attachToRunningJvmProcess } from "./attach-to-running-jvm-process.js";
|
|
6
|
+
export { respawnAndSupervise, inferRespawnCommandFromProc } from "./respawn-and-supervise.js";
|
|
7
|
+
export { CGROUP_PARTIAL_RESTRICTION_SCOPE, cgroupV2Capability, findDelegatedCgroupAncestor, moveProcessIntoRestrictedCgroup, } from "./cgroup-partial-restriction.js";
|
|
8
|
+
export { runInstrumentedExecution, createSourceRootResolver } from "./instrumented-execution.js";
|
|
9
|
+
export { backendObservationMechanismFor, runRemoteLogObservation } from "./profile-backend-observation.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAE9E,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAExF,OAAO,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAE3G,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAO/E,OAAO,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAK9F,OAAO,EACL,gCAAgC,EAChC,kBAAkB,EAClB,2BAA2B,EAC3B,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAMjG,OAAO,EAAE,8BAA8B,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The assembly nothing else performed.**
|
|
3
|
+
*
|
|
4
|
+
* Every piece of the runtime existed and none of them were wired together
|
|
5
|
+
* in production code: the controller spawned services, `RuntimeAdapter`
|
|
6
|
+
* could build collectors, `resolveServiceRootForOrigin` could map an origin
|
|
7
|
+
* to a source root, and `EvidenceStore` could persist. Only tests ever
|
|
8
|
+
* connected them, each in its own way, which meant the thing the product
|
|
9
|
+
* actually does had no single implementation to be right or wrong.
|
|
10
|
+
*
|
|
11
|
+
* This module is that implementation. It:
|
|
12
|
+
*
|
|
13
|
+
* 1. runs an `ExecutionController` over a `services` configuration;
|
|
14
|
+
* 2. wraps each spawned process in a **live** `ProcessOutputSource` that
|
|
15
|
+
* polls the accumulated buffer and yields each line exactly once;
|
|
16
|
+
* 3. builds a `CollectorContext` carrying a real `resolveSourceRoot`,
|
|
17
|
+
* closed over the execution's own processes — the channel RT-033 added
|
|
18
|
+
* and RT-029 built the lookup for, which until now nothing populated;
|
|
19
|
+
* 4. starts the adapter's collectors, drains them, stops them;
|
|
20
|
+
* 5. writes every emitted item through `EvidenceStore`, so redaction runs
|
|
21
|
+
* rather than being assumed (plan §28).
|
|
22
|
+
*
|
|
23
|
+
* **What it deliberately does not do.** It does not decide whether a
|
|
24
|
+
* finding exists, does not correlate to graph nodes, and does not drive a
|
|
25
|
+
* browser. Those are separate stages with separate contracts, and folding
|
|
26
|
+
* them in here would make the one place that assembles the runtime also the
|
|
27
|
+
* place that judges it.
|
|
28
|
+
*/
|
|
29
|
+
import type { Evidence, Execution, ExecutionConfiguration, ProcessHandle, ServiceRootLookup } from "@descryy/runtime-contracts";
|
|
30
|
+
import type { RuntimeAdapter } from "@descryy/runtime-backend-observation";
|
|
31
|
+
import { type CreateExecutionInput, type RunOptions } from "@descryy/runtime-controller";
|
|
32
|
+
import { EvidenceStore } from "@descryy/runtime-evidence-store";
|
|
33
|
+
export interface InstrumentedExecutionInput {
|
|
34
|
+
/** Carries the `ExecutionConfiguration` itself — the controller's own input shape, not a parallel copy that could drift from it. */
|
|
35
|
+
readonly execution: CreateExecutionInput;
|
|
36
|
+
readonly runOptions: RunOptions;
|
|
37
|
+
/** Supplies the collectors. One adapter per execution: a run observes one backend runtime. */
|
|
38
|
+
readonly adapter: RuntimeAdapter;
|
|
39
|
+
/** Where evidence is written. Provided rather than constructed so a caller owns the database's lifetime and its redaction policy. */
|
|
40
|
+
readonly store: EvidenceStore;
|
|
41
|
+
/**
|
|
42
|
+
* How long to keep draining collector output after the services are up.
|
|
43
|
+
* There is no "the application is done" signal at this layer — a server
|
|
44
|
+
* runs until stopped — so the caller states how long the observation
|
|
45
|
+
* window is rather than this module guessing.
|
|
46
|
+
*/
|
|
47
|
+
readonly observeForMs: number;
|
|
48
|
+
/**
|
|
49
|
+
* **Opt-in, per service name** (a key into `execution.configuration.services`):
|
|
50
|
+
* front that service with `InboundProxy` (RT-192, DEC-271) once — and
|
|
51
|
+
* only once — its OWN readiness has already succeeded. This module never
|
|
52
|
+
* re-asks the question; it reads the answer `controller.run()` already
|
|
53
|
+
* produced (`ExecutionController.serviceStartResults`), the same
|
|
54
|
+
* "readiness confirmed on the app's real port FIRST, proxy started AFTER"
|
|
55
|
+
* sequencing `cross-service-trace.test.ts` performs by hand.
|
|
56
|
+
*
|
|
57
|
+
* Absent, or a service not named here, is today's behaviour, unchanged:
|
|
58
|
+
* no proxy, traffic observed only through whatever collectors `adapter`
|
|
59
|
+
* itself supplies.
|
|
60
|
+
*
|
|
61
|
+
* **This does not close any per-language "HTTP observation" checklist
|
|
62
|
+
* row.** Every one of those rows uses a self-initiated fixture — the app
|
|
63
|
+
* calls itself via the exact `PORT` it bound — which has no separate
|
|
64
|
+
* public port for a proxy to sit in front of. This flag makes
|
|
65
|
+
* `InboundProxy` available for a *future externally-driven* scenario
|
|
66
|
+
* (the shape RT-196/`cross-service-trace.test.ts` already proved by
|
|
67
|
+
* hand); it does not retroactively make any current fixture
|
|
68
|
+
* externally-driven.
|
|
69
|
+
*/
|
|
70
|
+
readonly frontWithInboundProxy?: Readonly<Record<string, boolean>>;
|
|
71
|
+
/**
|
|
72
|
+
* Fires once every collector for this run — including any `InboundProxy`
|
|
73
|
+
* `frontWithInboundProxy` started — has started, and before the
|
|
74
|
+
* observation window's sleep begins. Exists so a caller (chiefly a test)
|
|
75
|
+
* can drive real traffic through a just-started proxy while it and the
|
|
76
|
+
* service it fronts are still alive; `InstrumentedExecutionResult` itself
|
|
77
|
+
* is only returned after the whole run has already stopped every
|
|
78
|
+
* process, which is too late for that. A throwing callback is not caught
|
|
79
|
+
* here, matching `RunOptions.onProcessLifecycleEvent`'s own "must not
|
|
80
|
+
* hang, may not silently swallow its own failure" rule.
|
|
81
|
+
*/
|
|
82
|
+
readonly onReady?: (info: {
|
|
83
|
+
readonly execution: Execution;
|
|
84
|
+
/** Service name → the proxy's own bound port, for every service that was both flagged AND successfully fronted. */
|
|
85
|
+
readonly proxyPorts: Readonly<Record<string, number>>;
|
|
86
|
+
}) => void | Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
export interface InstrumentedExecutionResult {
|
|
89
|
+
readonly execution: Execution;
|
|
90
|
+
readonly evidence: readonly Evidence[];
|
|
91
|
+
/** Every collector's own report of what it could do, after the run — not what it claimed at construction. */
|
|
92
|
+
readonly collectorCapabilities: readonly {
|
|
93
|
+
readonly collectorId: string;
|
|
94
|
+
readonly capabilities: ReturnType<import("@descryy/runtime-contracts").Collector["capabilities"]>;
|
|
95
|
+
}[];
|
|
96
|
+
/** Set when `run()` refused before spawning anything. Evidence is empty and that is a refusal, not an absence of failures. */
|
|
97
|
+
readonly validationError: string | null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Builds the origin → source-root lookup a collector reads through
|
|
101
|
+
* `CollectorContext.resolveSourceRoot`.
|
|
102
|
+
*
|
|
103
|
+
* This is the piece that had been missing at both ends: RT-029 wrote the
|
|
104
|
+
* lookup and nothing called it; RT-033 added the channel and nothing filled
|
|
105
|
+
* it. It closes over the execution's *own* processes, which is what makes
|
|
106
|
+
* the port→service join possible at all — under ephemeral ports the
|
|
107
|
+
* configuration cannot know the port, so only the running process can say.
|
|
108
|
+
*/
|
|
109
|
+
export declare function createSourceRootResolver(processes: readonly ProcessHandle[], configuration: ExecutionConfiguration): (origin: string) => ServiceRootLookup;
|
|
110
|
+
export declare function runInstrumentedExecution(input: InstrumentedExecutionInput): Promise<InstrumentedExecutionResult>;
|
|
111
|
+
//# sourceMappingURL=instrumented-execution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumented-execution.d.ts","sourceRoot":"","sources":["../src/instrumented-execution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAEV,QAAQ,EACR,SAAS,EACT,sBAAsB,EACtB,aAAa,EACb,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,sCAAsC,CAAC;AAEhG,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,UAAU,EAGhB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAKhE,MAAM,WAAW,0BAA0B;IACzC,oIAAoI;IACpI,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,8FAA8F;IAC9F,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,qIAAqI;IACrI,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnE;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;QAC9B,mHAAmH;QACnH,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KACvD,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,CAAC;IACvC,6GAA6G;IAC7G,QAAQ,CAAC,qBAAqB,EAAE,SAAS;QAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC,OAAO,4BAA4B,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC,CAAA;KAAE,EAAE,CAAC;IAC/K,8HAA8H;IAC9H,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,SAAS,aAAa,EAAE,EACnC,aAAa,EAAE,sBAAsB,GACpC,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAEvC;AAED,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAqVtH"}
|