@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.
Files changed (49) hide show
  1. package/dist/attach-to-running-jvm-process.d.ts +139 -0
  2. package/dist/attach-to-running-jvm-process.d.ts.map +1 -0
  3. package/dist/attach-to-running-jvm-process.js +338 -0
  4. package/dist/attach-to-running-jvm-process.js.map +1 -0
  5. package/dist/attach-to-running-node-process.d.ts +138 -0
  6. package/dist/attach-to-running-node-process.d.ts.map +1 -0
  7. package/dist/attach-to-running-node-process.js +315 -0
  8. package/dist/attach-to-running-node-process.js.map +1 -0
  9. package/dist/attach-to-running-process.d.ts +70 -0
  10. package/dist/attach-to-running-process.d.ts.map +1 -0
  11. package/dist/attach-to-running-process.js +90 -0
  12. package/dist/attach-to-running-process.js.map +1 -0
  13. package/dist/cdp-client.d.ts +37 -0
  14. package/dist/cdp-client.d.ts.map +1 -0
  15. package/dist/cdp-client.js +111 -0
  16. package/dist/cdp-client.js.map +1 -0
  17. package/dist/cgroup-partial-restriction.d.ts +127 -0
  18. package/dist/cgroup-partial-restriction.d.ts.map +1 -0
  19. package/dist/cgroup-partial-restriction.js +236 -0
  20. package/dist/cgroup-partial-restriction.js.map +1 -0
  21. package/dist/collector-version.d.ts +12 -0
  22. package/dist/collector-version.d.ts.map +1 -0
  23. package/dist/collector-version.js +14 -0
  24. package/dist/collector-version.js.map +1 -0
  25. package/dist/index.d.ts +19 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +10 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/instrumented-execution.d.ts +111 -0
  30. package/dist/instrumented-execution.d.ts.map +1 -0
  31. package/dist/instrumented-execution.js +363 -0
  32. package/dist/instrumented-execution.js.map +1 -0
  33. package/dist/jvm-agent/build.d.ts +72 -0
  34. package/dist/jvm-agent/build.d.ts.map +1 -0
  35. package/dist/jvm-agent/build.js +156 -0
  36. package/dist/jvm-agent/build.js.map +1 -0
  37. package/dist/polling-output-source.d.ts +28 -0
  38. package/dist/polling-output-source.d.ts.map +1 -0
  39. package/dist/polling-output-source.js +73 -0
  40. package/dist/polling-output-source.js.map +1 -0
  41. package/dist/profile-backend-observation.d.ts +94 -0
  42. package/dist/profile-backend-observation.d.ts.map +1 -0
  43. package/dist/profile-backend-observation.js +136 -0
  44. package/dist/profile-backend-observation.js.map +1 -0
  45. package/dist/respawn-and-supervise.d.ts +119 -0
  46. package/dist/respawn-and-supervise.d.ts.map +1 -0
  47. package/dist/respawn-and-supervise.js +212 -0
  48. package/dist/respawn-and-supervise.js.map +1 -0
  49. package/package.json +45 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Retroactive attach to an already-running **JVM (Java/Kotlin/…)** process —
3
+ * the JVM item on `RT-attach-to-running-process-scope.md`'s own
4
+ * "deliberately not built" list, and `RT-attach-to-running-jvm-process.md`
5
+ * (decisions-inbox) is the short record of what got measured before any of
6
+ * this was written and why the slice below is the honest first one, not a
7
+ * full CDP-equivalent.
8
+ *
9
+ * **The real mechanism, measured against a live JVM on this machine, not
10
+ * assumed from docs.** The JVM ships no built-in signal-to-inspector path
11
+ * the way Node's `SIGUSR1` does. What it does ship, since JDK 6, is the
12
+ * HotSpot Dynamic Attach mechanism: a launcher process calls
13
+ * `com.sun.tools.attach.VirtualMachine.attach(pid)`, which creates
14
+ * `.attach_pid<pid>` and signals the target with `SIGQUIT`; the target's own
15
+ * attach-listener thread (started lazily on first use, already running in
16
+ * every stock HotSpot JVM — nothing needs to be enabled at the target's own
17
+ * launch) opens a Unix-domain socket at `.java_pid<pid>` in its temp
18
+ * directory and accepts commands over it. `jcmd`/`jstack`/`VisualVM` all use
19
+ * exactly this. Measured directly here: attached to a plain `java
20
+ * QuietServer` process with no `-agentlib`/`-Djdk.attach` flags of any kind
21
+ * and no restart, confirming this is genuinely retroactive, the same
22
+ * standing Node's `SIGUSR1` slice has.
23
+ *
24
+ * **What this channel can actually give — measured, and narrower than
25
+ * CDP.** `VirtualMachine.loadAgent(jar, args)` loads a real Java agent
26
+ * (`java.lang.instrument.Instrumentation`) into the target's own JVM,
27
+ * running as that JVM's own code — a strictly more powerful primitive than
28
+ * CDP's remote-debugging protocol. This first slice used only the cheapest
29
+ * real capability inside it: `Thread.setDefaultUncaughtExceptionHandler`
30
+ * (`jvm-agent/Agent.java`), which needs no bytecode instrumentation at all,
31
+ * and **did not see an exception a framework already caught** — a request
32
+ * handler's own try/catch, a servlet container's default error page, any
33
+ * `@ExceptionHandler` — because by definition those exceptions never reach
34
+ * "uncaught." `SelfFailingServer.java` (the existing execute-mode fixture)
35
+ * catches and logs its own thrown exception for exactly this reason, so it
36
+ * is not a fixture this module's own test can reuse; see
37
+ * `test/fixtures/jvm-attach/BackgroundThreadThrows.java`, a real thread that
38
+ * throws with nothing catching it, the one shape that first slice could see.
39
+ *
40
+ * **RT-201 closed one specific, real instance of that gap**, using the same
41
+ * `Instrumentation` this module already hands the agent: `Agent.java` now
42
+ * also installs a ByteBuddy `ClassFileTransformer` (see that file's own
43
+ * header comment for the full mechanism and what was measured) that taps
44
+ * `org.springframework.web.servlet.DispatcherServlet#processHandlerException`
45
+ * — Spring MVC's single stable choke point for every exception-handling
46
+ * path — and reports what it catches as a `"framework-caught-exception"`
47
+ * event, parsed below as `isFrameworkCaughtExceptionEvent` and emitted as
48
+ * `EXCEPTION` evidence with `payload.handled === true`. **This closes only
49
+ * that one choke point.** A catch that never reaches Spring's dispatcher —
50
+ * absorbed inside a `@Service` method, logged nowhere — is still invisible,
51
+ * and no non-Spring framework is instrumented at all. `Runtime.exceptionThrown`'s
52
+ * CDP-side reach (any throw the debugger is configured to pause on,
53
+ * regardless of framework) still has no equivalent here.
54
+ *
55
+ * **No console-output capture in this slice.** Unlike CDP's
56
+ * `Runtime.consoleAPICalled`, `System.out`/`System.err` are not intercepted
57
+ * — a target already logging to a file is exactly `attachToRunningProcess`'s
58
+ * (the file-based sibling) job, and duplicating that here would be two
59
+ * mechanisms claiming the same evidence. `capabilities()` reports
60
+ * `backendLogAccess` unavailable for that reason, unlike the Node slice.
61
+ *
62
+ * **Launcher is a real JDK tool process, not a library call.** There is no
63
+ * Node/native binding for the Attach API in this repo and none is added —
64
+ * `jvm-agent/build.ts` compiles `Attacher.java` once (cached by mtime, same
65
+ * rule `kotlin-server/build.ts` already uses) and this module spawns it as a
66
+ * short-lived `java` child process, the same "one helper invocation, not a
67
+ * new protocol client" shape `discoverNodeInspectorUrl`'s `process.kill`
68
+ * call has, just through a JDK tool instead of a POSIX signal.
69
+ */
70
+ import type { Collector } from "@descryy/runtime-contracts";
71
+ export interface AttachToRunningJvmProcessOptions {
72
+ readonly processId: string;
73
+ /** The pid of the already-running target JVM. Verified alive before attach is attempted, and never spawned or supervised by this collector. */
74
+ readonly pid: number;
75
+ readonly service?: string;
76
+ /** How long to wait for the Attacher helper process to complete the attach + loadAgent call. The attach itself is normally sub-second; generous default for a loaded machine. */
77
+ readonly attachTimeoutMs?: number;
78
+ /** How often to check the agent's output file for new events. */
79
+ readonly pollIntervalMs?: number;
80
+ /**
81
+ * Repo-relative source roots plus the absolute repo root, enabling a JVM
82
+ * frame's bare basename to be reconstructed into a repo-relative path that
83
+ * graph correlation can actually join on (RT-202, extending RT-162's fix
84
+ * from the log-tailing path to this one).
85
+ *
86
+ * ## Why the attach path needed this too, and why it went unnoticed
87
+ *
88
+ * `JvmRuntimeAdapterOptions.sourceRoots` (`@descryhq-wq/runtime-adapter-jvm`)
89
+ * already fixed this for evidence produced by `createJvmStackTraceParser`
90
+ * — the JVM prints a bare basename (`ProbeApplication.java`), never a
91
+ * path, so `resolveSymbolNode` correctly refuses to match on it alone
92
+ * (RT-031's identity-proxy rule), and reconstruction from the frame's
93
+ * declaring type is what lets the two sides join. This collector builds
94
+ * its `StackFrame.location` directly from the Java agent's own captured
95
+ * `StackTraceElement` fields (`frameToStackFrame` below) and never went
96
+ * through that parser at all, so it never got the fix — invisible until
97
+ * RT-202, because this mechanism's only fixture before then
98
+ * (`BackgroundThreadThrows.java`) is in the default package, where
99
+ * reconstruction is a no-op regardless (no dot in the declaring type).
100
+ *
101
+ * ## Reimplemented here, not imported — a deliberate, disclosed duplication
102
+ *
103
+ * `packages/orchestrator`'s own `src/` has never taken a real dependency
104
+ * on any `@descryhq-wq/runtime-adapter-*` package — only
105
+ * `@descryy/runtime-adapter-typescript` is one, for an unrelated
106
+ * default-path reason, and every other language adapter (including
107
+ * `@descryhq-wq/runtime-adapter-jvm`) is a devDependency used solely by this
108
+ * package's own tests. Importing the JVM-specific reconstruction function
109
+ * into this language-agnostic-by-convention module would be a bigger
110
+ * architectural change than this gap warrants, so
111
+ * `reconstructJvmSourcePath` below is a second copy of the exact same
112
+ * algorithm (declaring type + basename + candidate roots + `existsSync`
113
+ * check, discarded rather than fabricated when nothing is on disk) —
114
+ * unlike other duplication this project has measured drifting silently,
115
+ * both copies are tiny, pure, and named after the one behaviour they
116
+ * implement, so a future change to one is easy to notice is missing from
117
+ * the other.
118
+ */
119
+ readonly sourceRoots?: {
120
+ readonly repoRoot: string;
121
+ readonly roots: readonly string[];
122
+ };
123
+ }
124
+ /**
125
+ * Attach to an already-running JVM process via the HotSpot Dynamic Attach
126
+ * mechanism, load a minimal Java agent, and start capturing genuinely
127
+ * uncaught exceptions as `EXCEPTION` evidence from that moment forward.
128
+ *
129
+ * **Retroactive, not historical** — same as the Node slice: an exception
130
+ * thrown before the agent finishes loading is not captured, because the
131
+ * default handler was not installed yet. There is no backlog to catch up on.
132
+ *
133
+ * Discovery, build, and attach all happen inside `start()`, matching every
134
+ * other `Collector` in this repo — a routine failure to attach resolves as
135
+ * `{ available: false, reason }`, never a thrown exception a caller has to
136
+ * remember to catch.
137
+ */
138
+ export declare function attachToRunningJvmProcess(options: AttachToRunningJvmProcessOptions): Collector;
139
+ //# sourceMappingURL=attach-to-running-jvm-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-to-running-jvm-process.d.ts","sourceRoot":"","sources":["../src/attach-to-running-jvm-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAOH,OAAO,KAAK,EAAE,SAAS,EAAyF,MAAM,4BAA4B,CAAC;AAMnJ,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+IAA+I;IAC/I,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,iLAAiL;IACjL,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,iEAAiE;IACjE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;KACnC,CAAC;CACH;AAsKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,SAAS,CAwH9F"}
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Retroactive attach to an already-running **JVM (Java/Kotlin/…)** process —
3
+ * the JVM item on `RT-attach-to-running-process-scope.md`'s own
4
+ * "deliberately not built" list, and `RT-attach-to-running-jvm-process.md`
5
+ * (decisions-inbox) is the short record of what got measured before any of
6
+ * this was written and why the slice below is the honest first one, not a
7
+ * full CDP-equivalent.
8
+ *
9
+ * **The real mechanism, measured against a live JVM on this machine, not
10
+ * assumed from docs.** The JVM ships no built-in signal-to-inspector path
11
+ * the way Node's `SIGUSR1` does. What it does ship, since JDK 6, is the
12
+ * HotSpot Dynamic Attach mechanism: a launcher process calls
13
+ * `com.sun.tools.attach.VirtualMachine.attach(pid)`, which creates
14
+ * `.attach_pid<pid>` and signals the target with `SIGQUIT`; the target's own
15
+ * attach-listener thread (started lazily on first use, already running in
16
+ * every stock HotSpot JVM — nothing needs to be enabled at the target's own
17
+ * launch) opens a Unix-domain socket at `.java_pid<pid>` in its temp
18
+ * directory and accepts commands over it. `jcmd`/`jstack`/`VisualVM` all use
19
+ * exactly this. Measured directly here: attached to a plain `java
20
+ * QuietServer` process with no `-agentlib`/`-Djdk.attach` flags of any kind
21
+ * and no restart, confirming this is genuinely retroactive, the same
22
+ * standing Node's `SIGUSR1` slice has.
23
+ *
24
+ * **What this channel can actually give — measured, and narrower than
25
+ * CDP.** `VirtualMachine.loadAgent(jar, args)` loads a real Java agent
26
+ * (`java.lang.instrument.Instrumentation`) into the target's own JVM,
27
+ * running as that JVM's own code — a strictly more powerful primitive than
28
+ * CDP's remote-debugging protocol. This first slice used only the cheapest
29
+ * real capability inside it: `Thread.setDefaultUncaughtExceptionHandler`
30
+ * (`jvm-agent/Agent.java`), which needs no bytecode instrumentation at all,
31
+ * and **did not see an exception a framework already caught** — a request
32
+ * handler's own try/catch, a servlet container's default error page, any
33
+ * `@ExceptionHandler` — because by definition those exceptions never reach
34
+ * "uncaught." `SelfFailingServer.java` (the existing execute-mode fixture)
35
+ * catches and logs its own thrown exception for exactly this reason, so it
36
+ * is not a fixture this module's own test can reuse; see
37
+ * `test/fixtures/jvm-attach/BackgroundThreadThrows.java`, a real thread that
38
+ * throws with nothing catching it, the one shape that first slice could see.
39
+ *
40
+ * **RT-201 closed one specific, real instance of that gap**, using the same
41
+ * `Instrumentation` this module already hands the agent: `Agent.java` now
42
+ * also installs a ByteBuddy `ClassFileTransformer` (see that file's own
43
+ * header comment for the full mechanism and what was measured) that taps
44
+ * `org.springframework.web.servlet.DispatcherServlet#processHandlerException`
45
+ * — Spring MVC's single stable choke point for every exception-handling
46
+ * path — and reports what it catches as a `"framework-caught-exception"`
47
+ * event, parsed below as `isFrameworkCaughtExceptionEvent` and emitted as
48
+ * `EXCEPTION` evidence with `payload.handled === true`. **This closes only
49
+ * that one choke point.** A catch that never reaches Spring's dispatcher —
50
+ * absorbed inside a `@Service` method, logged nowhere — is still invisible,
51
+ * and no non-Spring framework is instrumented at all. `Runtime.exceptionThrown`'s
52
+ * CDP-side reach (any throw the debugger is configured to pause on,
53
+ * regardless of framework) still has no equivalent here.
54
+ *
55
+ * **No console-output capture in this slice.** Unlike CDP's
56
+ * `Runtime.consoleAPICalled`, `System.out`/`System.err` are not intercepted
57
+ * — a target already logging to a file is exactly `attachToRunningProcess`'s
58
+ * (the file-based sibling) job, and duplicating that here would be two
59
+ * mechanisms claiming the same evidence. `capabilities()` reports
60
+ * `backendLogAccess` unavailable for that reason, unlike the Node slice.
61
+ *
62
+ * **Launcher is a real JDK tool process, not a library call.** There is no
63
+ * Node/native binding for the Attach API in this repo and none is added —
64
+ * `jvm-agent/build.ts` compiles `Attacher.java` once (cached by mtime, same
65
+ * rule `kotlin-server/build.ts` already uses) and this module spawns it as a
66
+ * short-lived `java` child process, the same "one helper invocation, not a
67
+ * new protocol client" shape `discoverNodeInspectorUrl`'s `process.kill`
68
+ * call has, just through a JDK tool instead of a POSIX signal.
69
+ */
70
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
71
+ import { tmpdir } from "node:os";
72
+ import { join } from "node:path";
73
+ import { spawnSync } from "node:child_process";
74
+ import { isProcessAlive } from "./attach-to-running-process.js";
75
+ import { buildJvmAgentJar, buildAttacherClass, findToolsJarIfPresent } from "./jvm-agent/build.js";
76
+ import { COLLECTOR_VERSION } from "./collector-version.js";
77
+ /**
78
+ * Reconstruct a repo-relative source path from a JVM frame's fully-qualified
79
+ * declaring class name and basename, or `null` when it cannot be done
80
+ * exactly — the attach path's copy of
81
+ * `jvm-stack-trace-parser.ts`'s `reconstructPath`; see this file's own
82
+ * `AttachToRunningJvmProcessOptions.sourceRoots` doc for why it is a copy
83
+ * rather than a shared import.
84
+ *
85
+ * Null in four cases, each a real one rather than a fallback: no source
86
+ * roots declared, no basename, no package (a default-package class, where
87
+ * `declaringType` has no dot), or no candidate that exists on disk.
88
+ */
89
+ function reconstructJvmSourcePath(declaringType, file, sourceRoots) {
90
+ if (sourceRoots === undefined || file === null)
91
+ return null;
92
+ if (file.includes("/"))
93
+ return null;
94
+ const lastDot = declaringType.lastIndexOf(".");
95
+ if (lastDot === -1)
96
+ return null;
97
+ const packagePath = declaringType.slice(0, lastDot).split(".").join("/");
98
+ for (const root of sourceRoots.roots) {
99
+ const candidate = `${root}/${packagePath}/${file}`;
100
+ if (existsSync(join(sourceRoots.repoRoot, candidate)))
101
+ return candidate;
102
+ }
103
+ return null;
104
+ }
105
+ function isUncaughtExceptionEvent(value) {
106
+ return typeof value === "object" && value !== null && value.type === "uncaught-exception";
107
+ }
108
+ function isFrameworkCaughtExceptionEvent(value) {
109
+ return typeof value === "object" && value !== null && value.type === "framework-caught-exception";
110
+ }
111
+ /**
112
+ * `StackTraceElement.getFileName()` is always a BARE name — `"ProbeApplication.java"`,
113
+ * never `"probe/ProbeApplication.java"` — because the JVM never recorded a
114
+ * package-relative path in the first place, only a plain source-file
115
+ * attribute. Found while proving RT-201's graph correlation directly (this
116
+ * collector's own pre-existing uncaught-exception path had never been
117
+ * pushed through `resolveSymbolNode`, so this was never exercised): a bare
118
+ * filename resolves nothing against a graph, whose nodes carry
119
+ * `src/main/java/probe/ProbeApplication.java`-shaped repo-relative paths.
120
+ *
121
+ * `className` (the fully-qualified class name, always present) is what
122
+ * recovers the missing directory — the same information `logback-log-envelope.ts`'s
123
+ * parser already uses on the log-tailing side, applied here for the attach
124
+ * side, which had never needed it before this collector's evidence was
125
+ * first asked to resolve against a graph. `className.split("$")[0]` drops
126
+ * a nested/anonymous-class suffix (`Outer$Inner`, `Outer$1`) to recover the
127
+ * OUTER class's package — nested classes are declared in their outer
128
+ * class's own file, never their own. The result is package-*relative*, not
129
+ * repo-relative — this is the fallback `frameToStackFrame` uses when
130
+ * `reconstructJvmSourcePath` above cannot produce a validated repo-relative
131
+ * path (no `sourceRoots` declared, or no candidate exists on disk); a
132
+ * caller with no `sourceRoots` still gets a directory instead of a bare
133
+ * basename, and can supply the rest via `resolveSymbolNode`'s own `cwd`
134
+ * option, exactly as documented there.
135
+ *
136
+ * Deliberately does NOT rename or reshape `frame.fileName` itself — Kotlin
137
+ * breaks the "file name matches class name" assumption outright (a class
138
+ * `Foo` may be declared in `Bar.kt`), so only the *directory* is derived
139
+ * from the class name; the file's own name is always the runtime's, verbatim.
140
+ */
141
+ function packageRelativeFile(frame) {
142
+ if (frame.fileName === null)
143
+ return null;
144
+ const outerClassName = frame.className.split("$")[0];
145
+ const lastDot = outerClassName.lastIndexOf(".");
146
+ if (lastDot === -1)
147
+ return frame.fileName; // default package — no directory to prepend
148
+ const packagePath = outerClassName.slice(0, lastDot).replaceAll(".", "/");
149
+ return `${packagePath}/${frame.fileName}`;
150
+ }
151
+ function frameToStackFrame(frame, sourceRoots) {
152
+ return {
153
+ location: {
154
+ file: reconstructJvmSourcePath(frame.className, frame.fileName, sourceRoots) ?? packageRelativeFile(frame),
155
+ line: frame.lineNumber > 0 ? frame.lineNumber : null,
156
+ column: null,
157
+ functionName: `${frame.className}.${frame.methodName}`,
158
+ // Self-contained: `StackTraceElement` is the JVM's own reported state,
159
+ // captured live off the running process — never a side artifact this
160
+ // collector resolved through, the same standing CDP's call frames get
161
+ // in the Node slice.
162
+ reliability: "self-contained",
163
+ resolvedVia: null,
164
+ },
165
+ raw: `${frame.className}.${frame.methodName}(${frame.fileName ?? "unknown"}:${String(frame.lineNumber)})`,
166
+ };
167
+ }
168
+ function notThisCollectorsJob(what) {
169
+ return { availability: "unavailable", reason: `attachToRunningJvmProcess observes only genuinely uncaught JVM exceptions via a loaded agent's default handler, not ${what}.` };
170
+ }
171
+ const CAPABILITIES = {
172
+ domObservation: notThisCollectorsJob("DOM inspection"),
173
+ consoleObservation: notThisCollectorsJob("a browser page's console"),
174
+ networkObservation: notThisCollectorsJob("network traffic"),
175
+ // Deliberately unavailable, unlike the Node slice — this module does not
176
+ // intercept System.out/err; that is attachToRunningProcess's job for a
177
+ // target already logging to a file, and duplicating it here would be two
178
+ // mechanisms claiming the same evidence.
179
+ backendLogAccess: notThisCollectorsJob("console/log output — see attachToRunningProcess for a target logging to a file"),
180
+ distributedTrace: notThisCollectorsJob("trace-id/request-id correlation — the loaded agent extracts none"),
181
+ sourceMapping: { availability: "available", reason: null },
182
+ stackCapture: { availability: "available", reason: null },
183
+ processLifecycle: notThisCollectorsJob("process lifecycle — spawn/ready/exit are ProcessCollector's to observe"),
184
+ databaseObservation: notThisCollectorsJob("database queries"),
185
+ externalServiceObservation: notThisCollectorsJob("outbound external requests"),
186
+ };
187
+ /**
188
+ * Runs the compiled `Attacher` helper against `pid`, loading `agent.jar`
189
+ * with `outputFilePath` as its argument. Resolves once the helper process
190
+ * exits 0 (attach + loadAgent both succeeded); rejects with the helper's own
191
+ * stderr otherwise — never guesses at success from silence.
192
+ */
193
+ function runAttacher(pid, agentJarPath, outputFilePath, attacherDir, attachTimeoutMs) {
194
+ const toolsJar = findToolsJarIfPresent();
195
+ const classpath = toolsJar !== null ? `${attacherDir}:${toolsJar}` : attacherDir;
196
+ const result = spawnSync("java", ["-cp", classpath, "Attacher", String(pid), agentJarPath, outputFilePath], {
197
+ encoding: "utf8",
198
+ timeout: attachTimeoutMs,
199
+ });
200
+ if (result.error !== undefined) {
201
+ return { ok: false, reason: `attachToRunningJvmProcess: could not launch the java Attacher helper: ${result.error.message}` };
202
+ }
203
+ if (result.status !== 0) {
204
+ const detail = (result.stderr ?? "").trim() || (result.stdout ?? "").trim() || `exit code ${String(result.status)}`;
205
+ return { ok: false, reason: `attachToRunningJvmProcess: Attacher failed to attach to pid ${String(pid)}: ${detail}` };
206
+ }
207
+ return { ok: true };
208
+ }
209
+ /**
210
+ * Attach to an already-running JVM process via the HotSpot Dynamic Attach
211
+ * mechanism, load a minimal Java agent, and start capturing genuinely
212
+ * uncaught exceptions as `EXCEPTION` evidence from that moment forward.
213
+ *
214
+ * **Retroactive, not historical** — same as the Node slice: an exception
215
+ * thrown before the agent finishes loading is not captured, because the
216
+ * default handler was not installed yet. There is no backlog to catch up on.
217
+ *
218
+ * Discovery, build, and attach all happen inside `start()`, matching every
219
+ * other `Collector` in this repo — a routine failure to attach resolves as
220
+ * `{ available: false, reason }`, never a thrown exception a caller has to
221
+ * remember to catch.
222
+ */
223
+ export function attachToRunningJvmProcess(options) {
224
+ let stopped = false;
225
+ return {
226
+ collectorId: `attach-jvm-agent:${options.processId}`,
227
+ async start(context) {
228
+ if (!isProcessAlive(options.pid)) {
229
+ return { available: false, reason: `attachToRunningJvmProcess: pid ${String(options.pid)} is not running — cannot attach to a target that does not exist.` };
230
+ }
231
+ if (process.platform === "win32") {
232
+ return { available: false, reason: "attachToRunningJvmProcess: HotSpot Dynamic Attach as used here relies on a Unix domain socket; not available on win32." };
233
+ }
234
+ let agentJarPath;
235
+ let attacherDir;
236
+ try {
237
+ agentJarPath = await buildJvmAgentJar();
238
+ attacherDir = buildAttacherClass();
239
+ }
240
+ catch (error) {
241
+ return { available: false, reason: error instanceof Error ? error.message : String(error) };
242
+ }
243
+ const dir = mkdtempSync(join(tmpdir(), "descry-jvm-attach-"));
244
+ const outFile = join(dir, "events.log");
245
+ writeFileSync(outFile, "");
246
+ const attached = runAttacher(options.pid, agentJarPath, outFile, attacherDir, options.attachTimeoutMs ?? 10_000);
247
+ if (!attached.ok) {
248
+ return { available: false, reason: attached.reason };
249
+ }
250
+ const pollIntervalMs = options.pollIntervalMs ?? 50;
251
+ let consumed = 0;
252
+ const poll = async () => {
253
+ for (;;) {
254
+ if (stopped)
255
+ return;
256
+ if (!existsSync(outFile)) {
257
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
258
+ continue;
259
+ }
260
+ const whole = readFileSync(outFile, "utf8");
261
+ if (whole.length > consumed) {
262
+ const chunk = whole.slice(consumed);
263
+ consumed = whole.length;
264
+ for (const rawLine of chunk.split("\n")) {
265
+ const line = rawLine.trim();
266
+ if (line === "")
267
+ continue;
268
+ let parsed;
269
+ try {
270
+ parsed = JSON.parse(line);
271
+ }
272
+ catch {
273
+ // A partially-flushed line read mid-write — will be
274
+ // re-attempted whole once the writer finishes, since
275
+ // `consumed` only advances past what was actually parsed
276
+ // here... except it already advanced past this chunk. In
277
+ // practice this cannot happen: Agent.java opens, writes one
278
+ // full line, and closes the file per event (see its own
279
+ // module doc), so a poll tick never observes a half-written
280
+ // line. Dropped rather than guessed at if it ever does.
281
+ continue;
282
+ }
283
+ if (isUncaughtExceptionEvent(parsed) || isFrameworkCaughtExceptionEvent(parsed)) {
284
+ const frames = parsed.frames.map((frame) => frameToStackFrame(frame, options.sourceRoots));
285
+ const stackTrace = frames.length > 0
286
+ ? { fidelity: "synchronous", frames, primaryFrameIndex: 0 }
287
+ : { fidelity: "synchronous", frames: [], primaryFrameIndex: 0 };
288
+ context.emit({
289
+ timestamp: new Date().toISOString(),
290
+ source: "backend-process",
291
+ service: options.service ?? null,
292
+ process: options.processId,
293
+ eventType: "EXCEPTION",
294
+ // `handled` distinguishes the two channels within the same
295
+ // `RuntimeEventType` rather than adding a new event type to
296
+ // that closed vocabulary (see `runtime-event.ts`'s own "the
297
+ // graph vocabulary is frozen; this one is not" — still, a
298
+ // new *member* is a deliberate, reviewed change, not one to
299
+ // make for a single collector's internal distinction) —
300
+ // `true` for RT-201's DispatcherServlet advice tap, `false`
301
+ // for a genuinely uncaught exception nothing in the JVM
302
+ // ever caught. Additive: existing consumers reading only
303
+ // `.message`/`.stack` off this payload are unaffected.
304
+ payload: { message: `${parsed.exceptionClass}: ${parsed.message ?? ""}`.trim(), stack: null, handled: parsed.type === "framework-caught-exception" },
305
+ traceId: null,
306
+ requestId: null,
307
+ correlationId: null,
308
+ graphNodeId: null,
309
+ sourceLocation: frames[0]?.location ?? null,
310
+ stackTrace: frames.length > 0 ? stackTrace : null,
311
+ confidence: 1,
312
+ redactionStatus: "pending-redaction",
313
+ collectorVersion: COLLECTOR_VERSION,
314
+ });
315
+ }
316
+ // "agent-loaded", "framework-instrumentation-installed",
317
+ // "framework-instrumentation-unavailable", and any future
318
+ // marker type: acknowledged by being parsed, not turned into
319
+ // evidence — internal bookkeeping, not something observed
320
+ // about the target.
321
+ }
322
+ }
323
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
324
+ }
325
+ };
326
+ void poll();
327
+ return { available: true };
328
+ },
329
+ stop() {
330
+ stopped = true;
331
+ return Promise.resolve();
332
+ },
333
+ capabilities() {
334
+ return CAPABILITIES;
335
+ },
336
+ };
337
+ }
338
+ //# sourceMappingURL=attach-to-running-jvm-process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-to-running-jvm-process.js","sourceRoot":"","sources":["../src/attach-to-running-jvm-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI/C,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAwD3D;;;;;;;;;;;GAWG;AACH,SAAS,wBAAwB,CAC/B,aAAqB,EACrB,IAAmB,EACnB,WAA4D;IAE5D,IAAI,WAAW,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5D,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpC,MAAM,OAAO,GAAG,aAAa,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,WAAW,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEzE,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,WAAW,IAAI,IAAI,EAAE,CAAC;QACnD,IAAI,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;IAC1E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AA0BD,SAAS,wBAAwB,CAAC,KAAc;IAC9C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAA4B,CAAC,IAAI,KAAK,oBAAoB,CAAC;AACpH,CAAC;AAED,SAAS,+BAA+B,CAAC,KAAc;IACrD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAA4B,CAAC,IAAI,KAAK,4BAA4B,CAAC;AAC5H,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,SAAS,mBAAmB,CAAC,KAAwB;IACnD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,cAAc,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACtD,MAAM,OAAO,GAAG,cAAc,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAChD,IAAI,OAAO,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,4CAA4C;IACvF,MAAM,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC1E,OAAO,GAAG,WAAW,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;AAC5C,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAwB,EAAE,WAA4D;IAC/G,OAAO;QACL,QAAQ,EAAE;YACR,IAAI,EAAE,wBAAwB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE,WAAW,CAAC,IAAI,mBAAmB,CAAC,KAAK,CAAC;YAC1G,IAAI,EAAE,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;YACpD,MAAM,EAAE,IAAI;YACZ,YAAY,EAAE,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE;YACtD,uEAAuE;YACvE,qEAAqE;YACrE,sEAAsE;YACtE,qBAAqB;YACrB,WAAW,EAAE,gBAAgB;YAC7B,WAAW,EAAE,IAAI;SAClB;QACD,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,IAAI,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG;KAC1G,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAY;IACxC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,uHAAuH,IAAI,GAAG,EAAE,CAAC;AACjL,CAAC;AAED,MAAM,YAAY,GAA0B;IAC1C,cAAc,EAAE,oBAAoB,CAAC,gBAAgB,CAAC;IACtD,kBAAkB,EAAE,oBAAoB,CAAC,0BAA0B,CAAC;IACpE,kBAAkB,EAAE,oBAAoB,CAAC,iBAAiB,CAAC;IAC3D,yEAAyE;IACzE,uEAAuE;IACvE,yEAAyE;IACzE,yCAAyC;IACzC,gBAAgB,EAAE,oBAAoB,CAAC,gFAAgF,CAAC;IACxH,gBAAgB,EAAE,oBAAoB,CAAC,kEAAkE,CAAC;IAC1G,aAAa,EAAE,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IAC1D,YAAY,EAAE,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE;IACzD,gBAAgB,EAAE,oBAAoB,CAAC,wEAAwE,CAAC;IAChH,mBAAmB,EAAE,oBAAoB,CAAC,kBAAkB,CAAC;IAC7D,0BAA0B,EAAE,oBAAoB,CAAC,4BAA4B,CAAC;CAC/E,CAAC;AAEF;;;;;GAKG;AACH,SAAS,WAAW,CAAC,GAAW,EAAE,YAAoB,EAAE,cAAsB,EAAE,WAAmB,EAAE,eAAuB;IAC1H,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IACjF,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,YAAY,EAAE,cAAc,CAAC,EAAE;QAC1G,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,eAAe;KACzB,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,yEAAyE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;IAChI,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,aAAa,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACpH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,+DAA+D,MAAM,CAAC,GAAG,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;IACxH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,yBAAyB,CAAC,OAAyC;IACjF,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,OAAO;QACL,WAAW,EAAE,oBAAoB,OAAO,CAAC,SAAS,EAAE;QAEpD,KAAK,CAAC,KAAK,CAAC,OAAyB;YACnC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACjC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,kCAAkC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,kEAAkE,EAAE,CAAC;YAC/J,CAAC;YACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACjC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,wHAAwH,EAAE,CAAC;YAChK,CAAC;YAED,IAAI,YAAoB,CAAC;YACzB,IAAI,WAAmB,CAAC;YACxB,IAAI,CAAC;gBACH,YAAY,GAAG,MAAM,gBAAgB,EAAE,CAAC;gBACxC,WAAW,GAAG,kBAAkB,EAAE,CAAC;YACrC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9F,CAAC;YAED,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;YAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YACxC,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAE3B,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,eAAe,IAAI,MAAM,CAAC,CAAC;YACjH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;YACvD,CAAC;YAED,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,EAAE,CAAC;YACpD,IAAI,QAAQ,GAAG,CAAC,CAAC;YAEjB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;gBACrC,SAAS,CAAC;oBACR,IAAI,OAAO;wBAAE,OAAO;oBACpB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;wBACzB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;wBACpE,SAAS;oBACX,CAAC;oBACD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;oBAC5C,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC;wBAC5B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;wBACpC,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC;wBACxB,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;4BACxC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;4BAC5B,IAAI,IAAI,KAAK,EAAE;gCAAE,SAAS;4BAC1B,IAAI,MAAe,CAAC;4BACpB,IAAI,CAAC;gCACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;4BAC5B,CAAC;4BAAC,MAAM,CAAC;gCACP,oDAAoD;gCACpD,qDAAqD;gCACrD,yDAAyD;gCACzD,yDAAyD;gCACzD,4DAA4D;gCAC5D,wDAAwD;gCACxD,4DAA4D;gCAC5D,wDAAwD;gCACxD,SAAS;4BACX,CAAC;4BACD,IAAI,wBAAwB,CAAC,MAAM,CAAC,IAAI,+BAA+B,CAAC,MAAM,CAAC,EAAE,CAAC;gCAChF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;gCAC3F,MAAM,UAAU,GAAe,MAAM,CAAC,MAAM,GAAG,CAAC;oCAC9C,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC,EAAE;oCAC3D,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;gCAClE,OAAO,CAAC,IAAI,CAAC;oCACX,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;oCACnC,MAAM,EAAE,iBAAiB;oCACzB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,IAAI;oCAChC,OAAO,EAAE,OAAO,CAAC,SAAS;oCAC1B,SAAS,EAAE,WAAW;oCACtB,2DAA2D;oCAC3D,4DAA4D;oCAC5D,4DAA4D;oCAC5D,0DAA0D;oCAC1D,4DAA4D;oCAC5D,wDAAwD;oCACxD,4DAA4D;oCAC5D,wDAAwD;oCACxD,yDAAyD;oCACzD,uDAAuD;oCACvD,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,cAAc,KAAK,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,KAAK,4BAA4B,EAAE;oCACpJ,OAAO,EAAE,IAAI;oCACb,SAAS,EAAE,IAAI;oCACf,aAAa,EAAE,IAAI;oCACnB,WAAW,EAAE,IAAI;oCACjB,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,IAAI,IAAI;oCAC3C,UAAU,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI;oCACjD,UAAU,EAAE,CAAC;oCACb,eAAe,EAAE,mBAAmB;oCACpC,gBAAgB,EAAE,iBAAiB;iCACpC,CAAC,CAAC;4BACL,CAAC;4BACD,yDAAyD;4BACzD,0DAA0D;4BAC1D,6DAA6D;4BAC7D,0DAA0D;4BAC1D,oBAAoB;wBACtB,CAAC;oBACH,CAAC;oBACD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC,CAAC;YACF,KAAK,IAAI,EAAE,CAAC;YAEZ,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAED,IAAI;YACF,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,YAAY;YACV,OAAO,YAAY,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Retroactive attach to an already-running **Node** process, via its own
3
+ * inspector protocol — the specific, buildable slice
4
+ * `documents/decisions-inbox/RT-attach-to-running-process-scope.md`
5
+ * deliberately did not build. That entry's own "deliberately not built"
6
+ * list names it directly: "Node's `SIGUSR1` inspector activation + a CDP
7
+ * WebSocket client — real, but Node-only and a new protocol surface, not
8
+ * built this pass." This is that pass, for Node only — the other two items
9
+ * on that list (JVM Attach API, ptrace) remain out of scope, unchanged.
10
+ *
11
+ * **What makes this genuinely retroactive, unlike `attachToRunningProcess`
12
+ * (the file-based sibling in this same package).** That one needs the
13
+ * target to have already been told, at its own launch time, to write
14
+ * output somewhere reachable (`> server.log 2>&1`) — narrower than "attach
15
+ * to anything already running." This one needs nothing declared at launch:
16
+ * `SIGUSR1` (POSIX only; Node's own signal handler, not something this
17
+ * process installs) turns on the target's inspector **after the fact**, on
18
+ * a process that was never started with `--inspect` and never expected to
19
+ * be debugged. From that moment its debugger discovery endpoint is
20
+ * reachable at `http://127.0.0.1:<port>/json`, by the target itself, not by
21
+ * anything this collector primed in advance.
22
+ *
23
+ * **The one real limit, disclosed rather than hidden.** `SIGUSR1` does not
24
+ * report back which port the inspector actually bound — Node prints
25
+ * `Debugger listening on ws://host:port/<uuid>` to the target's own
26
+ * *stderr*, which is exactly the channel this module's own doc (and RT-024
27
+ * before it) says cannot be read for a process we did not spawn. So
28
+ * discovery is: try the default port (9229, Node's own default and the one
29
+ * `SIGUSR1` binds to when nothing else is already listening there), and if
30
+ * a caller already knows the target was configured for a different port
31
+ * (`--inspect-port`, `NODE_OPTIONS`), they supply it. **If the port is
32
+ * genuinely unknown and is not 9229, this module cannot discover it** — not
33
+ * attempted, not guessed at by scanning a port range, which would risk
34
+ * attaching to the wrong process's inspector entirely.
35
+ *
36
+ * **CDP client: written here, not reused, because nothing reusable
37
+ * exists.** `packages/browser`'s console collector speaks CDP only through
38
+ * Playwright's own browser-automation abstraction (`page.on("console")`),
39
+ * which assumes a `Target`/`Page`-domain browser session — a Node inspector
40
+ * session has neither domain, being a single top-level `Runtime` session
41
+ * instead. `cdp-client.ts` in this package is the actual reusable piece:
42
+ * protocol-family-generic, so a future browser-side raw-CDP need could
43
+ * reuse it too, instead of a third client getting written.
44
+ *
45
+ * **Measured: a freshly-spawned target has a brief window, sometimes under
46
+ * 100ms, before Node installs its own `SIGUSR1` handler** — a signal sent
47
+ * inside that window hits POSIX's default action for an unhandled
48
+ * `SIGUSR1` (terminate) instead of toggling the inspector, killing the
49
+ * target rather than attaching to it. Irrelevant to this module's actual
50
+ * purpose — attaching to something genuinely already running, which by
51
+ * definition has been alive far longer than that window — but real, and
52
+ * worth naming for a caller tempted to attach immediately after spawning
53
+ * something themselves.
54
+ *
55
+ * **Measured side effect: attaching changes the target's crash behaviour.**
56
+ * Once a CDP client is connected, V8 no longer terminates the process
57
+ * immediately on an uncaught exception — it prints "Waiting for the
58
+ * debugger to disconnect..." to stderr and blocks, exactly as it would
59
+ * under a launched `--inspect-brk` session, so a human debugger gets to
60
+ * inspect the paused, crashed state before the process actually exits.
61
+ * Measured directly (a naive test that waited for the child's own `exit`
62
+ * event after attach hung indefinitely) rather than assumed from docs. This
63
+ * is standard CDP/V8 behaviour, not a defect this collector introduces, and
64
+ * not silently worked around: the process resumes its exit exactly when
65
+ * `stop()` closes the CDP session, which is also when the exception's own
66
+ * evidence has already been captured, so nothing about the timing costs a
67
+ * caller the evidence. A caller relying on "did the target exit yet?" as a
68
+ * signal must call `stop()` first.
69
+ *
70
+ * **What is captured, and under which vocabulary.** `Runtime.consoleAPICalled`
71
+ * → `CONSOLE_MESSAGE`, `Runtime.exceptionThrown` → `EXCEPTION` — the same
72
+ * two `RuntimeEventType`s `browser-console-collector.ts` already claims for
73
+ * the analogous browser-page concepts, because they are the same concepts
74
+ * (console output; an uncaught exception), observed on a different kind of
75
+ * process. `source: "backend-process"`, not a new source value — this is a
76
+ * backend process being observed, and `EvidenceSource` already names what
77
+ * was observed rather than which mechanism did it (`inbound-proxy.ts`'s own
78
+ * precedent, restated there for exactly this reason). `capabilities()`
79
+ * reports `backendLogAccess`, not `consoleObservation` — `consoleObservation`
80
+ * is `browser-console-collector`'s claim on a *browser page's* console,
81
+ * kept apart from a backend process's console the same way `LogCollector`
82
+ * already keeps them apart for its own text-based capture.
83
+ */
84
+ import type { Collector } from "@descryy/runtime-contracts";
85
+ export interface AttachToRunningNodeProcessOptions {
86
+ readonly processId: string;
87
+ /** The pid of the already-running target. Verified alive before SIGUSR1 is sent, and never spawned or supervised by this collector. */
88
+ readonly pid: number;
89
+ readonly service?: string;
90
+ /** Node's own default inspector bind address. Overridable for a target known to bind elsewhere. */
91
+ readonly discoveryHost?: string;
92
+ /**
93
+ * Node's own default inspector port, and the only one `SIGUSR1` reliably
94
+ * produces on an unmodified target. See this file's own module doc for
95
+ * why a non-default port cannot be discovered from outside and must be
96
+ * supplied here when known.
97
+ */
98
+ readonly discoveryPort?: number;
99
+ /** How long to poll the discovery endpoint after sending SIGUSR1 before giving up. The inspector does not bind instantaneously. */
100
+ readonly discoveryTimeoutMs?: number;
101
+ }
102
+ /**
103
+ * Sends `SIGUSR1` to `pid`, then polls `http://<host>:<port>/json` until the
104
+ * target's inspector answers or `timeoutMs` elapses. Resolves with the
105
+ * `webSocketDebuggerUrl` of the first entry — an already-running Node
106
+ * process presents exactly one inspectable target for its main thread,
107
+ * unlike a browser's `/json`, which lists one entry per open tab.
108
+ *
109
+ * Rejects, naming the exact blocker, rather than hanging or guessing: a pid
110
+ * that was never alive, a POSIX-only signal on a non-POSIX platform, or a
111
+ * port that never answers (most often because the real inspector bound
112
+ * somewhere other than `port`, which this function has no way to learn —
113
+ * see the module doc).
114
+ */
115
+ export declare function discoverNodeInspectorUrl(pid: number, options?: {
116
+ readonly host?: string;
117
+ readonly port?: number;
118
+ readonly timeoutMs?: number;
119
+ }): Promise<string>;
120
+ /**
121
+ * Attach to an already-running Node process via `SIGUSR1` + CDP, and start
122
+ * capturing `CONSOLE_MESSAGE`/`EXCEPTION` evidence from that moment forward.
123
+ *
124
+ * **Retroactive, not historical.** Anything the target logged or threw
125
+ * *before* this attaches is gone — `SIGUSR1` turns the inspector on, it
126
+ * does not hand over a backlog. `attachToRunningProcess` (the file-based
127
+ * sibling) is the one that can recover pre-attach lines, and only because a
128
+ * file on disk already held them; nothing analogous exists for CDP, which
129
+ * is a live event stream with no buffer of its own.
130
+ *
131
+ * Discovery and connection happen inside `start()`, not here — matching
132
+ * every other `Collector` in this repo (`createBrowserConsoleCollector`,
133
+ * `createTestRunnerCollector`): a routine failure to attach resolves as
134
+ * `{ available: false, reason }`, never a thrown exception a caller has to
135
+ * remember to catch.
136
+ */
137
+ export declare function attachToRunningNodeProcess(options: AttachToRunningNodeProcessOptions): Collector;
138
+ //# sourceMappingURL=attach-to-running-node-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-to-running-node-process.d.ts","sourceRoot":"","sources":["../src/attach-to-running-node-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkFG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAyG,MAAM,4BAA4B,CAAC;AAMnK,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uIAAuI;IACvI,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,mGAAmG;IACnG,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,mIAAmI;IACnI,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAOD;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5F,OAAO,CAAC,MAAM,CAAC,CAyCjB;AA6ED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,SAAS,CAqHhG"}