@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,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":"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"}
|
|
@@ -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"}
|