@gravitylabsllc/porthole 0.1.0 → 0.2.1
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/README.md +123 -0
- package/dist/adb.js +430 -21
- package/dist/adb.js.map +1 -1
- package/dist/args.js +144 -0
- package/dist/args.js.map +1 -0
- package/dist/capture.js +139 -30
- package/dist/capture.js.map +1 -1
- package/dist/cli.js +221 -62
- package/dist/cli.js.map +1 -1
- package/dist/device.js +337 -4
- package/dist/device.js.map +1 -1
- package/dist/index.js +2030 -377
- package/dist/index.js.map +1 -1
- package/dist/moment.js +240 -0
- package/dist/moment.js.map +1 -0
- package/dist/perfetto.js +826 -0
- package/dist/perfetto.js.map +1 -0
- package/dist/report.js +68 -7
- package/dist/report.js.map +1 -1
- package/dist/save.js +252 -0
- package/dist/save.js.map +1 -0
- package/dist/sessions.js +704 -0
- package/dist/sessions.js.map +1 -0
- package/dist/system.js +169 -0
- package/dist/system.js.map +1 -0
- package/dist/systrace.js +198 -0
- package/dist/systrace.js.map +1 -0
- package/dist/timeline.js +731 -29
- package/dist/timeline.js.map +1 -1
- package/dist/trace.js +317 -27
- package/dist/trace.js.map +1 -1
- package/dist/watermark.js +220 -0
- package/dist/watermark.js.map +1 -0
- package/package.json +10 -4
- package/src/adb.ts +583 -0
- package/src/args.ts +177 -0
- package/src/capture.ts +292 -0
- package/src/cli.ts +367 -0
- package/src/device.ts +635 -0
- package/src/index.ts +2545 -0
- package/src/moment.ts +306 -0
- package/src/perfetto.ts +972 -0
- package/src/report.ts +285 -0
- package/src/save.ts +322 -0
- package/src/sessions.ts +894 -0
- package/src/system.ts +221 -0
- package/src/systrace.ts +258 -0
- package/src/timeline.ts +1036 -0
- package/src/trace.ts +769 -0
- package/src/watermark.ts +337 -0
- package/ui/dist/assets/index-DtnyBXCM.css +1 -0
- package/ui/dist/assets/index-h7VNB9Fl.js +70 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index--1mlZuNZ.css +0 -1
- 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
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
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 (
|
|
12
|
-
|
|
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
|
-
*
|
|
315
|
+
* How long `runAdbAsync` waits before presuming an adb child is wedged.
|
|
42
316
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"adb.js","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
|