@gravitylabsllc/porthole 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +123 -0
  2. package/dist/adb.js +430 -21
  3. package/dist/adb.js.map +1 -1
  4. package/dist/args.js +144 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/capture.js +139 -30
  7. package/dist/capture.js.map +1 -1
  8. package/dist/cli.js +221 -62
  9. package/dist/cli.js.map +1 -1
  10. package/dist/device.js +337 -4
  11. package/dist/device.js.map +1 -1
  12. package/dist/index.js +2030 -377
  13. package/dist/index.js.map +1 -1
  14. package/dist/moment.js +240 -0
  15. package/dist/moment.js.map +1 -0
  16. package/dist/perfetto.js +826 -0
  17. package/dist/perfetto.js.map +1 -0
  18. package/dist/report.js +68 -7
  19. package/dist/report.js.map +1 -1
  20. package/dist/save.js +252 -0
  21. package/dist/save.js.map +1 -0
  22. package/dist/sessions.js +704 -0
  23. package/dist/sessions.js.map +1 -0
  24. package/dist/system.js +169 -0
  25. package/dist/system.js.map +1 -0
  26. package/dist/systrace.js +198 -0
  27. package/dist/systrace.js.map +1 -0
  28. package/dist/timeline.js +731 -29
  29. package/dist/timeline.js.map +1 -1
  30. package/dist/trace.js +317 -27
  31. package/dist/trace.js.map +1 -1
  32. package/dist/watermark.js +220 -0
  33. package/dist/watermark.js.map +1 -0
  34. package/package.json +10 -4
  35. package/src/adb.ts +583 -0
  36. package/src/args.ts +177 -0
  37. package/src/capture.ts +292 -0
  38. package/src/cli.ts +367 -0
  39. package/src/device.ts +635 -0
  40. package/src/index.ts +2545 -0
  41. package/src/moment.ts +306 -0
  42. package/src/perfetto.ts +972 -0
  43. package/src/report.ts +285 -0
  44. package/src/save.ts +322 -0
  45. package/src/sessions.ts +894 -0
  46. package/src/system.ts +221 -0
  47. package/src/systrace.ts +258 -0
  48. package/src/timeline.ts +1036 -0
  49. package/src/trace.ts +769 -0
  50. package/src/watermark.ts +337 -0
  51. package/ui/dist/assets/index-BzqwnvoU.js +70 -0
  52. package/ui/dist/assets/index-DtnyBXCM.css +1 -0
  53. package/ui/dist/index.html +2 -2
  54. package/ui/dist/assets/index--1mlZuNZ.css +0 -1
  55. package/ui/dist/assets/index-BeVGHRFm.js +0 -68
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @gravitylabsllc/porthole
2
+
3
+ The workstation half of [Porthole](https://porthole.gravitylabs.live): an MCP server
4
+ and a live timeline for a running Android app.
5
+
6
+ Porthole is a debug-only agent that lives inside your app and answers questions about
7
+ it while it runs — what recomposed and why, which frames were dropped and where the
8
+ time went, what is holding the main thread, what is in flight, what your state
9
+ actually contains right now. This package is what connects to it: a browser timeline
10
+ for a person watching their own app, and an MCP server for a coding assistant that
11
+ can read the answers and go change the code that caused them.
12
+
13
+ The agent itself is an Android library. It is not in this package, and nothing here
14
+ returns data until it is running: add the Gradle plugin `live.gravitylabs.porthole`
15
+ to your app module, install a debug build, and then come back here. Setup is in the
16
+ [repository README](https://github.com/gravityrepos/porthole#setup).
17
+
18
+ ## The timeline
19
+
20
+ ```bash
21
+ npx @gravitylabsllc/porthole ui
22
+ ```
23
+
24
+ That forwards the device port with `adb`, serves the timeline, opens your browser and
25
+ keeps running until you stop it. It needs Node 18 or newer and `adb` — from
26
+ `ANDROID_HOME`, a `local.properties` above the working directory, or your PATH.
27
+
28
+ ## The MCP server
29
+
30
+ `./gradlew portholeMcpConfig` writes this entry for you, merging rather than
31
+ overwriting. To paste it into `.mcp.json` yourself:
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "porthole": {
37
+ "command": "npx",
38
+ "args": ["-y", "@gravitylabsllc/porthole"],
39
+ "env": { "PORTHOLE_PORT": "8677" }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ The tools it registers:
46
+
47
+ | tool | answers |
48
+ | --- | --- |
49
+ | `findings` | start here: what is wrong right now, ranked, each with the tool that shows its evidence |
50
+ | `recompositions` | which composables recomposed, how often, and which state keys were written just before |
51
+ | `semantics_tree` | the semantics tree with an id that stays stable across captures |
52
+ | `nav_state` | back stack, arguments on each entry, and the deep link that got you here |
53
+ | `state` | current values of your ViewModel state, named automatically |
54
+ | `inflight` | open HTTP calls with the phase each is stuck in, running queries, WorkManager jobs |
55
+ | `frames` | dropped frames, and which phase of the frame ate the time |
56
+ | `blocking` | what held the main thread, with the stack it was stuck in |
57
+ | `logs` | the app's own logcat output, stack traces intact, without touching adb |
58
+ | `timeline` | the raw event stream, for ordering things relative to each other |
59
+ | `what_was_happening` | the narrative for one instant: screen, in-flight work, main thread, state just written |
60
+ | `system_context` | thermal state, CPU governor, busiest processes, memory pressure |
61
+ | `capture_system_trace` | records a Perfetto trace, annotated with the app's own spans |
62
+ | `ask_system_trace` | puts a fixed set of questions to a captured trace, to rule causes in or out |
63
+ | `open_timeline` | opens the live timeline in the browser |
64
+ | `porthole_status` | whether any of the above can currently reach the device |
65
+
66
+ ## The two binaries
67
+
68
+ `porthole` is the CLI, for a person:
69
+
70
+ ```
71
+ porthole ui open the live timeline
72
+ porthole capture --scenario <name> -- <command> record a run to a trace
73
+ porthole report <trace.json> what the run is worth looking at
74
+ porthole compare <base> <trace> regressions against a baseline
75
+ porthole mcp the MCP server (stdio)
76
+ ```
77
+
78
+ `porthole-mcp` is that last line as its own entry point, for MCP clients that want a
79
+ bare command rather than a subcommand. The two are the same server.
80
+
81
+ A bad `--port`/`--ui-port` value or an unrecognized option exits `2` rather than
82
+ starting something that listens on the wrong thing and says nothing. Quoted
83
+ from the built CLI itself:
84
+
85
+ ```
86
+ $ porthole ui --port abc
87
+ --port "abc" is not a number
88
+
89
+ $ porthole ui --port 99999
90
+ --port 99999 is out of range (must be 1024-65535)
91
+
92
+ $ porthole ui --port 4.5
93
+ --port 4.5 must be a whole number
94
+
95
+ $ porthole ui --port
96
+ --port needs a port number
97
+
98
+ $ porthole ui --bogus-flag
99
+ unknown option: --bogus-flag
100
+ <usage follows>
101
+ ```
102
+
103
+ ## Environment
104
+
105
+ | variable | default | what it sets |
106
+ | --- | --- | --- |
107
+ | `PORTHOLE_HOST` | `127.0.0.1` | host the forwarded device socket is reachable on |
108
+ | `PORTHOLE_PORT` | `8677` | device port the porthole listens on |
109
+ | `PORTHOLE_UI_PORT` | `8678` | port the timeline is served on |
110
+ | `PORTHOLE_TRACE_PROCESSOR` | none | path to Perfetto's `trace_processor`, for system traces |
111
+ | `PORTHOLE_TRACE_TIMEOUT_MS` | `60000` | how long `ask_system_trace` waits on `trace_processor` per question before giving up |
112
+
113
+ The MCP server and the timeline are independent. Run either, or both at once — they
114
+ each open their own connection to the device.
115
+
116
+ ## More
117
+
118
+ - [porthole.gravitylabs.live](https://porthole.gravitylabs.live) — what it is, with pictures
119
+ - [github.com/gravityrepos/porthole](https://github.com/gravityrepos/porthole) — the full documentation: setup, what the numbers mean, what gets captured and what does not, security
120
+
121
+ ## License
122
+
123
+ Apache License 2.0. Copyright 2026 Gravity Labs.
package/dist/adb.js CHANGED
@@ -1,19 +1,293 @@
1
1
  // Copyright 2026 Gravity Labs
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- import { spawnSync } from "node:child_process";
4
- import { existsSync } from "node:fs";
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import path from "node:path";
6
- /** adb, in the order a developer would look for it. Mirrors the Gradle plugin. */
7
- export function findAdb() {
8
- const binary = process.platform === "win32" ? "adb.exe" : "adb";
6
+ /**
7
+ * How far up from the working directory to look for local.properties.
8
+ *
9
+ * The working directory is usually the build root, but an agent or a shell is
10
+ * just as often sitting in a module inside it. Five levels covers that without
11
+ * letting a build that has no local.properties at all walk to the top of the
12
+ * disk looking for one.
13
+ */
14
+ const LOCAL_PROPERTIES_LEVELS = 5;
15
+ /**
16
+ * A .properties file, read the way java.util.Properties reads it.
17
+ *
18
+ * local.properties is written by Android Studio, and on Windows what it writes
19
+ * is `sdk.dir=C\:\\Users\\james\\AppData\\Local\\Android\\Sdk`. Splitting on
20
+ * the first `=` and keeping the rest verbatim yields a path with an escaped
21
+ * colon and doubled separators that no filesystem call will accept, so the
22
+ * escapes have to come off. The rules honoured here are Java's, because Java's
23
+ * are what wrote the file and what the Gradle plugin reads it back with: `#`
24
+ * and `!` comment out a whole line and nothing else — a `#` partway through a
25
+ * value belongs to the value — `=`, `:` or whitespace ends the key, and a line
26
+ * ending in an odd number of backslashes continues onto the next.
27
+ */
28
+ export function parseProperties(text) {
29
+ const properties = new Map();
30
+ const lines = text.split(/\r\n|\n|\r/);
31
+ for (let i = 0; i < lines.length; i++) {
32
+ let line = lines[i].replace(/^[ \t\f]+/, "");
33
+ if (line === "" || line.startsWith("#") || line.startsWith("!"))
34
+ continue;
35
+ while (trailingBackslashes(line) % 2 === 1 && i + 1 < lines.length) {
36
+ line = line.slice(0, -1) + lines[++i].replace(/^[ \t\f]+/, "");
37
+ }
38
+ // The key runs to the first separator that is not itself escaped.
39
+ let end = 0;
40
+ while (end < line.length) {
41
+ const character = line[end];
42
+ if (character === "\\") {
43
+ end += 2;
44
+ continue;
45
+ }
46
+ if ("=: \t\f".includes(character))
47
+ break;
48
+ end++;
49
+ }
50
+ let value = line.slice(end).replace(/^[ \t\f]*/, "");
51
+ if (value.startsWith("=") || value.startsWith(":")) {
52
+ value = value.slice(1).replace(/^[ \t\f]*/, "");
53
+ }
54
+ properties.set(unescape(line.slice(0, end)), unescape(value));
55
+ }
56
+ return properties;
57
+ }
58
+ function trailingBackslashes(line) {
59
+ let count = 0;
60
+ while (count < line.length && line[line.length - 1 - count] === "\\")
61
+ count++;
62
+ return count;
63
+ }
64
+ function unescape(raw) {
65
+ let out = "";
66
+ for (let i = 0; i < raw.length; i++) {
67
+ if (raw[i] !== "\\") {
68
+ out += raw[i];
69
+ continue;
70
+ }
71
+ const escaped = raw[++i];
72
+ if (escaped === undefined)
73
+ break;
74
+ if (escaped === "u" && /^[0-9a-fA-F]{4}$/.test(raw.slice(i + 1, i + 5))) {
75
+ out += String.fromCharCode(parseInt(raw.slice(i + 1, i + 5), 16));
76
+ i += 4;
77
+ continue;
78
+ }
79
+ // Java drops the backslash before anything it does not recognise, which is
80
+ // what turns `C\:\\Users` back into `C:\Users`.
81
+ out += { t: "\t", n: "\n", r: "\r", f: "\f" }[escaped] ?? escaped;
82
+ }
83
+ return out;
84
+ }
85
+ /**
86
+ * Where PORTHOLE_PROJECT_ROOT and PORTHOLE_SDK_DIR come from: `.mcp.json`,
87
+ * generated at Gradle configure time by `PortholeMcpConfigTask`. That task
88
+ * knows both values with certainty — the Gradle root project directory, and
89
+ * `sdk.dir`/`ANDROID_HOME` resolved the same way `PortholePlugin` resolves
90
+ * them for `adb` itself — because it is the build, not a process launched by
91
+ * whatever an MCP client decided to use as `cwd`.
92
+ */
93
+ const PORTHOLE_PROJECT_ROOT = "PORTHOLE_PROJECT_ROOT";
94
+ const PORTHOLE_SDK_DIR = "PORTHOLE_SDK_DIR";
95
+ /**
96
+ * The project root anything here should treat as "the build", in order:
97
+ * `PORTHOLE_PROJECT_ROOT` if the generated config set it, otherwise
98
+ * `process.cwd()` on the stated assumption GRA-87 already made and this
99
+ * ticket exists to stop relying on — that an MCP client launches its stdio
100
+ * server from the workspace root. When a generated `.mcp.json` is what
101
+ * started this process, that assumption is no longer needed at all.
102
+ */
103
+ export function resolveProjectRoot() {
104
+ const declared = process.env[PORTHOLE_PROJECT_ROOT];
105
+ if (declared && declared.trim()) {
106
+ return { directory: declared.trim(), source: "PORTHOLE_PROJECT_ROOT" };
107
+ }
108
+ return { directory: process.cwd(), source: "cwd" };
109
+ }
110
+ /**
111
+ * True for a Windows drive-relative path — a letter, a colon, and then
112
+ * anything other than a separator (`C:foo`, or bare `C:`): "foo, relative to
113
+ * whatever the current directory on drive C happens to be", a real Windows
114
+ * path concept distinct from both absolute and an ordinary relative path.
115
+ * This is the TypeScript analogue of `isWindowsDriveRelative` in
116
+ * PortholeTasks.kt (GRA-150), kept for the same reason: `path.join(directory,
117
+ * "C:foo")` does not anchor it under `directory` — Node happily concatenates
118
+ * the strings into `<directory>\C:foo`, a colon spliced into the middle of a
119
+ * path segment that Windows refuses to open. There is no reliable way to ask
120
+ * Node, any more than the JVM, what "the current directory on drive C" is for
121
+ * a directory other than this process's own, so — matching the Kotlin
122
+ * resolver's choice — this shape is deliberately left to resolve however
123
+ * `path.resolve` on its own (no `directory` argument) already resolves it,
124
+ * rather than inventing an anchor. Gated on `win32`: everywhere else a colon
125
+ * is an ordinary filename character, and `C:foo` there is exactly as
126
+ * relative as it looks.
127
+ */
128
+ function isWindowsDriveRelative(value) {
129
+ return process.platform === "win32" && /^[A-Za-z]:($|[^\\/])/.test(value);
130
+ }
131
+ /**
132
+ * True for a path `java.io.File#isAbsolute()` — and so PortholeTasks.kt's
133
+ * `resolveSdkDir` — would call absolute. On Windows this deliberately
134
+ * disagrees with Node's own `path.isAbsolute`: a bare POSIX-style leading
135
+ * slash (`/opt/sdk`) is absolute to Node there (it roots the path at
136
+ * whatever the current drive is) but not to Java, which requires a drive
137
+ * letter or a UNC prefix. GRA-150's QA moved the Kotlin resolver to route
138
+ * that shape through the project-root join instead of the current-drive
139
+ * root; using Node's native check here would silently put TypeScript back on
140
+ * the old, rejected answer for this one shape. See PortholeTasks.kt's
141
+ * `resolveSdkDir` doc comment for the full story. Off Windows, Java's rule
142
+ * and Node's agree (both are simply "starts with /"), so this just defers to
143
+ * `path.isAbsolute`.
144
+ */
145
+ function isJavaStyleAbsolute(value) {
146
+ if (process.platform !== "win32")
147
+ return path.isAbsolute(value);
148
+ return /^[A-Za-z]:[\\/]/.test(value) || /^[\\/]{2}/.test(value);
149
+ }
150
+ /**
151
+ * Anchors a `sdk.dir` value read out of local.properties against `directory`
152
+ * — [sdkDirFromLocalProperties]'s current directory in its walk, i.e. the
153
+ * directory the file was actually found in. This is deliberately NOT
154
+ * `resolveProjectRoot().directory`: the two differ whenever the walk climbs
155
+ * past the project root to find the file, and a relative path written inside
156
+ * that file means "relative to where the file lives", not "relative to
157
+ * wherever PORTHOLE_PROJECT_ROOT happens to point". This mirrors
158
+ * PortholeTasks.kt's `resolveSdkDir`, which anchors on `projectRoot` — its
159
+ * exact analogue, for the exact same reason (GRA-160 AC1).
160
+ *
161
+ * `path.join`, not `path.resolve`, does the anchoring for the ordinary case:
162
+ * `path.resolve` special-cases any argument it considers absolute — which,
163
+ * on Windows, includes the POSIX-style shape `isJavaStyleAbsolute`
164
+ * deliberately excludes — by discarding every argument before it and rooting
165
+ * at the current drive instead. That is exactly the answer this function
166
+ * exists to avoid. `path.join` treats every argument as a plain segment
167
+ * regardless of what it looks like, so it always anchors under `directory`;
168
+ * the outer `path.resolve` then only normalizes the result (drops a trailing
169
+ * separator, collapses a redundant `.`/`..`), which is also where the
170
+ * "GRA-160 AC4" trim below actually pays for itself — an unstripped
171
+ * whitespace character survives straight into this join as part of the path
172
+ * segment, and lands on a directory that does not exist.
173
+ */
174
+ function resolveRelativeSdkDir(value, directory) {
175
+ if (isWindowsDriveRelative(value))
176
+ return path.resolve(value);
177
+ if (isJavaStyleAbsolute(value))
178
+ return path.resolve(value);
179
+ return path.resolve(path.join(directory, value));
180
+ }
181
+ /** `sdk.dir` from the nearest local.properties at or above `from`. */
182
+ function sdkDirFromLocalProperties(from) {
183
+ let directory = path.resolve(from);
184
+ for (let level = 0; level <= LOCAL_PROPERTIES_LEVELS; level++) {
185
+ const file = path.join(directory, "local.properties");
186
+ try {
187
+ if (existsSync(file)) {
188
+ // A local.properties with no sdk.dir in it is not an answer, so the
189
+ // walk continues past it rather than stopping at the first file found.
190
+ const value = parseProperties(readFileSync(file, "utf8")).get("sdk.dir");
191
+ // GRA-160 AC4: value.trim() here is the only thing standing between
192
+ // a value parseProperties handed back un-trimmed and a filesystem
193
+ // check on a directory that does not exist. Plain ASCII whitespace
194
+ // around the value mostly never reaches this line at all —
195
+ // parseProperties' own `[ \t\f]` stripping already ate the leading
196
+ // run, and a value with nothing but trailing ASCII space is realistic
197
+ // (a hand-edited file, or a stray editor auto-format) but easy to
198
+ // write a fixture for and forget. A non-breaking space (U+00A0) is
199
+ // the sharper case: `[ \t\f]` does not include it, so it survives
200
+ // parseProperties untouched either way, and only JS's own
201
+ // Unicode-aware `trim()` — not a regex character class copied from
202
+ // Java's — removes it here. adb.test.ts covers both.
203
+ if (value && value.trim())
204
+ return resolveRelativeSdkDir(value.trim(), directory);
205
+ }
206
+ }
207
+ catch {
208
+ // Unreadable is the same as absent: the environment is next, and a
209
+ // permissions problem on someone's parent directory is not adb's fault.
210
+ }
211
+ const parent = path.dirname(directory);
212
+ if (parent === directory)
213
+ break; // The root of the filesystem is its own parent.
214
+ directory = parent;
215
+ }
216
+ return null;
217
+ }
218
+ function isDirectory(candidate) {
219
+ try {
220
+ return statSync(candidate).isDirectory();
221
+ }
222
+ catch {
223
+ return false;
224
+ }
225
+ }
226
+ /**
227
+ * The Android SDK, in the order a developer would look for it — and, ahead
228
+ * of all of them, the order this ticket adds: an explicit `PORTHOLE_SDK_DIR`
229
+ * from a generated `.mcp.json` wins outright and short-circuits before any
230
+ * filesystem access, deliberately. GRA-87's walk up from the project root for
231
+ * local.properties, and the ANDROID_HOME/ANDROID_SDK_ROOT fallback after it,
232
+ * both stay exactly as they were for a server that was not started from a
233
+ * generated config — see [sdkDirFromLocalProperties] and its tests.
234
+ *
235
+ * The walk-and-environment order below is deliberately the Gradle plugin's —
236
+ * `sdkDirectory` in PortholePlugin.kt — and the two agreeing is the whole
237
+ * point of it. sdk.dir is what Android Studio writes into local.properties,
238
+ * and on a stock install it is the only one of these that is set: neither
239
+ * environment variable exists, and platform-tools is not on PATH, least of
240
+ * all on Windows. Reading only the environment is why `system_context` used
241
+ * to tell the user to go fix something that `./gradlew portholeConnect`, on
242
+ * the same machine and the same SDK, had no trouble with.
243
+ *
244
+ * Once local.properties names a directory, that is the answer even if adb
245
+ * turns out not to be beneath it. The plugin stops there too, and one tool
246
+ * quietly falling through to a different SDK than the other is the exact
247
+ * split personality this replaced.
248
+ */
249
+ export function resolveSdkDir() {
250
+ const declared = process.env[PORTHOLE_SDK_DIR];
251
+ if (declared && declared.trim()) {
252
+ return { directory: declared.trim(), source: "PORTHOLE_SDK_DIR" };
253
+ }
254
+ const fromProperties = sdkDirFromLocalProperties(resolveProjectRoot().directory);
255
+ if (fromProperties)
256
+ return { directory: fromProperties, source: "local.properties" };
9
257
  for (const variable of ["ANDROID_HOME", "ANDROID_SDK_ROOT"]) {
10
258
  const root = process.env[variable];
11
- if (!root)
12
- continue;
13
- const candidate = path.join(root, "platform-tools", binary);
14
- if (existsSync(candidate))
15
- return candidate;
259
+ if (root && isDirectory(root))
260
+ return { directory: root, source: variable };
16
261
  }
262
+ return { directory: null, source: "PATH" };
263
+ }
264
+ /**
265
+ * adb, from the SDK if one can be found, and otherwise left to the PATH.
266
+ *
267
+ * GRA-160 AC3: falling through to a bare binary name is the right answer
268
+ * only when nothing named an SDK at all (`directory` is null, `source` is
269
+ * "PATH") — that is an honest "I don't know", and PATH is the reasonable
270
+ * last resort, unremarked. When `directory` IS known — Android Studio wrote
271
+ * it, or this ticket's fix just anchored a relative one against the right
272
+ * place — but platform-tools is not actually there, falling through to PATH
273
+ * the same silent way is a different thing: it runs *some* adb, possibly a
274
+ * different SDK's, without a word to whoever asked. The wave-4 integration
275
+ * QA measured exactly this. This writes that specific case to stderr rather
276
+ * than swallowing it — stdout is the MCP transport's JSON-RPC channel, so
277
+ * stderr is the only channel available here that cannot corrupt it. Fully
278
+ * surfacing it through `porthole_status`'s payload would need a change in
279
+ * index.ts, which this ticket's Owns excludes; see the ticket report.
280
+ */
281
+ export function findAdb() {
282
+ const binary = process.platform === "win32" ? "adb.exe" : "adb";
283
+ const { directory } = resolveSdkDir();
284
+ if (!directory)
285
+ return binary;
286
+ const candidate = path.join(directory, "platform-tools", binary);
287
+ if (existsSync(candidate))
288
+ return candidate;
289
+ process.stderr.write(`[porthole] sdk.dir resolved to ${directory}, but ` +
290
+ `${path.join("platform-tools", binary)} was not found there; falling back to ${binary} on PATH.\n`);
17
291
  return binary;
18
292
  }
19
293
  export function runAdb(args, serial) {
@@ -38,21 +312,122 @@ export function runAdb(args, serial) {
38
312
  return { ok: true, output };
39
313
  }
40
314
  /**
41
- * Stop the app and start it again.
315
+ * How long `runAdbAsync` waits before presuming an adb child is wedged.
42
316
  *
43
- * Driven from this side rather than from inside the app: a process cannot
44
- * reliably restart itself, and asking it to try is how you end up with a
45
- * half-dead process that no longer answers the socket.
317
+ * 150s, not `capture_system_trace`'s own 120s maximum: a caller recording a
318
+ * near-maximum-length trace overrides this per-call to `seconds * 1000` plus
319
+ * a startup buffer (see `index.ts`), so this default only ever governs the
320
+ * short calls — the pull and the on-device cleanup — where anything near a
321
+ * minute already means adb itself, not the recording, is stuck.
46
322
  */
47
- export function restartApp(packageName, serial) {
48
- const stopped = runAdb(["shell", "am", "force-stop", packageName], serial);
49
- if (!stopped.ok)
50
- return stopped;
51
- const started = runAdb(["shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"], serial);
323
+ const DEFAULT_ADB_TIMEOUT_MS = Number(process.env.PORTHOLE_ADB_TIMEOUT_MS) || 150_000;
324
+ /** "One per few seconds", per the ticket: not so chatty it drowns stderr, not so sparse a caller watching the log wonders if the server is still alive. */
325
+ const DEFAULT_ADB_TICK_MS = 5_000;
326
+ /** Writes one line to stderr — never stdout, which on this server is the MCP transport's own JSON-RPC channel. */
327
+ function defaultAdbProgress(elapsedMs, args) {
328
+ process.stderr.write(`[porthole] adb ${args[0] ?? ""} still running after ${Math.round(elapsedMs / 1000)}s...\n`);
329
+ }
330
+ /**
331
+ * `runAdb`'s async twin: `spawn`, awaited, standing in for `spawnSync`.
332
+ *
333
+ * `capture_system_trace`'s three adb calls — the recording, the pull, and
334
+ * the on-device cleanup — used to run through `runAdb` above, which blocks
335
+ * Node's single thread for as long as the child takes. At the tool's own
336
+ * 120s maximum that froze the whole MCP server for over two minutes: nothing
337
+ * read the device socket, nothing answered another tool call, and the
338
+ * timeline WebSocket went silent. This is the way back, and it is
339
+ * deliberately the same shape GRA-82 already proved out for
340
+ * `trace_processor_shell` in `perfetto.ts`'s `runScript` — `spawn` instead
341
+ * of `spawnSync`, a `setTimeout` that kills the child and reports how long
342
+ * it waited, one shared helper rather than a second way to run a child
343
+ * process asynchronously.
344
+ *
345
+ * Progress is reported on stderr, not returned, because nothing here knows
346
+ * whether anyone is listening for it — it exists so a long recording does
347
+ * not look identically alive and wedged from the outside.
348
+ */
349
+ export function runAdbAsync(args, options = {}) {
350
+ const { serial, binary = findAdb(), timeoutMs = DEFAULT_ADB_TIMEOUT_MS, onProgress = defaultAdbProgress, tickMs = DEFAULT_ADB_TICK_MS, env, } = options;
351
+ const prefix = serial ? ["-s", serial] : [];
352
+ const fullArgs = [...prefix, ...args];
353
+ return new Promise((resolvePromise) => {
354
+ const start = Date.now();
355
+ let settled = false;
356
+ let stdout = "";
357
+ let stderr = "";
358
+ let timedOut = false;
359
+ // `env` is only passed through when given: `spawn(binary, fullArgs)`
360
+ // with no third argument at all is what every real, non-test call makes
361
+ // (production behaviour is unchanged either way, since Node's own
362
+ // default is already "inherit process.env").
363
+ const child = env ? spawn(binary, fullArgs, { env }) : spawn(binary, fullArgs);
364
+ const ticker = setInterval(() => {
365
+ if (!settled)
366
+ onProgress(Date.now() - start, fullArgs);
367
+ }, tickMs);
368
+ const timer = setTimeout(() => {
369
+ timedOut = true;
370
+ child.kill();
371
+ }, timeoutMs);
372
+ const finish = (result) => {
373
+ if (settled)
374
+ return;
375
+ settled = true;
376
+ clearInterval(ticker);
377
+ clearTimeout(timer);
378
+ resolvePromise(result);
379
+ };
380
+ child.stdout?.setEncoding("utf8");
381
+ child.stderr?.setEncoding("utf8");
382
+ child.stdout?.on("data", (chunk) => {
383
+ stdout += chunk;
384
+ });
385
+ child.stderr?.on("data", (chunk) => {
386
+ stderr += chunk;
387
+ });
388
+ // The binary itself did not run (ENOENT, EACCES, ...) — same wording as
389
+ // the sync version above, so a caller cannot tell which path answered.
390
+ child.on("error", (error) => {
391
+ finish({
392
+ ok: false,
393
+ output: `Could not run adb (${error.message}). Set ANDROID_HOME, or put adb on your PATH.`,
394
+ });
395
+ });
396
+ child.on("close", (code) => {
397
+ if (timedOut) {
398
+ finish({
399
+ ok: false,
400
+ output: `adb did not finish within ${timeoutMs}ms running '${fullArgs.join(" ")}'; ` +
401
+ "it may be wedged, so it was killed rather than left to hang.",
402
+ });
403
+ return;
404
+ }
405
+ const output = (stdout + stderr).trim();
406
+ if (code !== 0) {
407
+ finish({
408
+ ok: false,
409
+ output: output.includes("more than one") && !serial
410
+ ? `${output}\nStart the UI with --serial <id>; 'adb devices' lists them.`
411
+ : output || `adb exited ${code}`,
412
+ });
413
+ return;
414
+ }
415
+ finish({ ok: true, output });
416
+ });
417
+ });
418
+ }
419
+ /**
420
+ * The `am force-stop` / launcher-intent pair both `restartApp` and
421
+ * `restartAppAsync` send, and the one honest way to tell whether the second
422
+ * half actually launched anything.
423
+ *
424
+ * monkey reports success on stdout even when it launched nothing, so the
425
+ * absence of its "Events injected" line — not its exit code — is the signal
426
+ * that there was no launcher activity to hit.
427
+ */
428
+ function finishRestart(packageName, started) {
52
429
  if (!started.ok)
53
430
  return started;
54
- // monkey reports success on stdout even when it launched nothing, so the
55
- // absence of its "Events injected" line is the honest failure signal.
56
431
  if (!started.output.includes("Events injected")) {
57
432
  return {
58
433
  ok: false,
@@ -61,4 +436,38 @@ export function restartApp(packageName, serial) {
61
436
  }
62
437
  return { ok: true, output: `restarted ${packageName}` };
63
438
  }
439
+ /**
440
+ * Stop the app and start it again.
441
+ *
442
+ * Driven from this side rather than from inside the app: a process cannot
443
+ * reliably restart itself, and asking it to try is how you end up with a
444
+ * half-dead process that no longer answers the socket.
445
+ */
446
+ export function restartApp(packageName, serial) {
447
+ const stopped = runAdb(["shell", "am", "force-stop", packageName], serial);
448
+ if (!stopped.ok)
449
+ return stopped;
450
+ const started = runAdb(["shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"], serial);
451
+ return finishRestart(packageName, started);
452
+ }
453
+ /**
454
+ * `restartApp`'s async twin, spawned with `runAdbAsync` instead of
455
+ * `runAdb`'s blocking `spawnSync`.
456
+ *
457
+ * GRA-186: `capture_system_trace` restarts the app it is tracing partway
458
+ * through an already-running recording (see `index.ts`), so it cannot use
459
+ * the sync version without freezing the event loop for as long as the
460
+ * force-stop/relaunch pair takes — exactly the problem GRA-89 already fixed
461
+ * for the recording, pull and cleanup calls in the same tool. This shares
462
+ * `finishRestart`'s arg-building result and "Events injected" check with the
463
+ * sync version rather than re-deriving them, so the two can only drift by a
464
+ * change that touches both call sites.
465
+ */
466
+ export async function restartAppAsync(packageName, options = {}) {
467
+ const stopped = await runAdbAsync(["shell", "am", "force-stop", packageName], options);
468
+ if (!stopped.ok)
469
+ return stopped;
470
+ const started = await runAdbAsync(["shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"], options);
471
+ return finishRestart(packageName, started);
472
+ }
64
473
  //# sourceMappingURL=adb.js.map
package/dist/adb.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"adb.js","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,sCAAsC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,kFAAkF;AAClF,MAAM,UAAU,OAAO;IACrB,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;IAChE,KAAK,MAAM,QAAQ,IAAI,CAAC,cAAc,EAAE,kBAAkB,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,CAAC,CAAC;QAC5D,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAOD,MAAM,UAAU,MAAM,CAAC,IAAc,EAAE,MAAe;IACpD,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAEhF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EACJ,sBAAsB,MAAM,CAAC,KAAK,CAAC,OAAO,KAAK;gBAC/C,4CAA4C;SAC/C,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACtE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EACJ,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM;gBACzC,CAAC,CAAC,GAAG,MAAM,8DAA8D;gBACzE,CAAC,CAAC,MAAM,IAAI,cAAc,MAAM,CAAC,MAAM,EAAE;SAC9C,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB,EAAE,MAAe;IAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3E,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,OAAO,CAAC;IAEhC,MAAM,OAAO,GAAG,MAAM,CACpB,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,kCAAkC,EAAE,GAAG,CAAC,EACrF,MAAM,CACP,CAAC;IACF,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,OAAO,CAAC;IAEhC,yEAAyE;IACzE,sEAAsE;IACtE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAChD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,kCAAkC,WAAW,GAAG;SAC3E,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,WAAW,EAAE,EAAE,CAAC;AAC1D,CAAC"}
1
+ {"version":3,"file":"adb.js","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}