mobile-debug-mcp 0.3.0 → 0.5.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/.github/copilot-instructions.md +33 -0
- package/README.md +142 -25
- package/dist/android.js +174 -32
- package/dist/ios.js +203 -14
- package/dist/server.js +262 -20
- package/dist/types.js +1 -0
- package/docs/CHANGELOG.md +23 -0
- package/package.json +8 -2
- package/smoke-test.js +102 -0
- package/smoke-test.ts +115 -0
- package/src/android.ts +205 -31
- package/src/ios.ts +234 -16
- package/src/server.ts +304 -24
- package/src/types.ts +58 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copilot Instructions for mobile-debug-mcp
|
|
2
|
+
|
|
3
|
+
## Build and Run
|
|
4
|
+
- **Build**: `npm run build` (runs `tsc` to compile TypeScript to `dist/`)
|
|
5
|
+
- **Start**: `npm start` (runs the compiled server at `dist/server.js`)
|
|
6
|
+
- **Dev Workflow**: modify `src/*.ts` -> `npm run build` -> `npm start` to test changes.
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
- **Core**: `src/server.ts` implements the MCP server using `@modelcontextprotocol/sdk`. It handles tool registration and execution.
|
|
10
|
+
- **Platform Modules**:
|
|
11
|
+
- `src/android.ts`: Encapsulates `adb` commands for Android device interaction.
|
|
12
|
+
- `src/ios.ts`: Encapsulates `xcrun simctl` commands for iOS simulator interaction.
|
|
13
|
+
- **Types**: `src/types.ts` defines shared interfaces for device info and tool responses.
|
|
14
|
+
|
|
15
|
+
## Key Conventions
|
|
16
|
+
|
|
17
|
+
### Tool Implementation
|
|
18
|
+
- **Response Format**: Tools typically return a list of content blocks.
|
|
19
|
+
- `start_app`: Returns a single text block containing JSON (via `wrapResponse`).
|
|
20
|
+
- `terminate_app`: Returns a single text block containing JSON (via `wrapResponse`).
|
|
21
|
+
- `restart_app`: Returns a single text block containing JSON (via `wrapResponse`).
|
|
22
|
+
- `reset_app_data`: Returns a single text block containing JSON (via `wrapResponse`).
|
|
23
|
+
- `get_logs`: Returns a text block (JSON metadata) AND a text block (raw logs).
|
|
24
|
+
- `capture_screenshot`: Returns a text block (JSON metadata) AND an image block (base64 PNG).
|
|
25
|
+
- **Metadata**: Always include a `device` object (platform, id, model, etc.) in the JSON response part.
|
|
26
|
+
|
|
27
|
+
### External Tools
|
|
28
|
+
- **Android**: Uses `process.env.ADB_PATH` or defaults to `adb`.
|
|
29
|
+
- **iOS**: Uses `process.env.XCRUN_PATH` or defaults to `xcrun`. Assumes a booted simulator.
|
|
30
|
+
- **Execution**: Uses `child_process.exec` for running shell commands.
|
|
31
|
+
|
|
32
|
+
### Error Handling
|
|
33
|
+
- Tools should catch execution errors and return a user-friendly error message in a `text` content block, rather than crashing the server.
|
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Mobile Debug MCP
|
|
2
2
|
|
|
3
|
-
**Mobile Debug MCP** is a minimal MCP server for AI-assisted mobile development. It allows you to **launch Android or iOS apps
|
|
3
|
+
**Mobile Debug MCP** is a minimal, secure MCP server for AI-assisted mobile development. It allows you to **launch Android or iOS apps**, **read their logs**, and **inspect UI** from an MCP-compatible AI client.
|
|
4
|
+
|
|
5
|
+
This server is designed with security in mind, using strict argument handling to prevent shell injection, and reliability, with robust process management to avoid hanging operations.
|
|
6
|
+
|
|
7
|
+
> **Note:** iOS support is currently an untested Work In Progress (WIP). Please use with caution and report any issues.
|
|
4
8
|
|
|
5
9
|
---
|
|
6
10
|
|
|
@@ -9,6 +13,9 @@
|
|
|
9
13
|
- Launch Android apps via package name.
|
|
10
14
|
- Launch iOS apps via bundle ID on a booted simulator.
|
|
11
15
|
- Fetch recent logs from Android or iOS apps.
|
|
16
|
+
- Terminate and restart apps.
|
|
17
|
+
- Clear app data for fresh installs.
|
|
18
|
+
- Capture screenshots.
|
|
12
19
|
- Cross-platform support (Android + iOS).
|
|
13
20
|
- Minimal, focused design for fast debugging loops.
|
|
14
21
|
|
|
@@ -48,9 +55,29 @@ This option installs the package globally for easy use without cloning the repo.
|
|
|
48
55
|
|
|
49
56
|
---
|
|
50
57
|
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
## Testing
|
|
59
|
+
33.
|
|
60
|
+
34. The repository includes a smoke test script to verify end-to-end functionality on real devices or simulators.
|
|
61
|
+
35.
|
|
62
|
+
36. ```bash
|
|
63
|
+
37. # Run smoke test for Android
|
|
64
|
+
38. npx tsx smoke-test.ts android com.example.package
|
|
65
|
+
39.
|
|
66
|
+
40. # Run smoke test for iOS
|
|
67
|
+
41. npx tsx smoke-test.ts ios com.example.bundleid
|
|
68
|
+
42. ```
|
|
69
|
+
43.
|
|
70
|
+
44. The smoke test performs the following sequence:
|
|
71
|
+
45. 1. Starts the app
|
|
72
|
+
46. 2. Captures a screenshot
|
|
73
|
+
47. 3. Fetches logs
|
|
74
|
+
48. 4. Terminates the app
|
|
75
|
+
49.
|
|
76
|
+
50. ---
|
|
77
|
+
51.
|
|
78
|
+
52. ## MCP Server Configuration
|
|
79
|
+
|
|
80
|
+
Example WebUI MCP config using `npx --yes` and environment variables:
|
|
54
81
|
|
|
55
82
|
```json
|
|
56
83
|
{
|
|
@@ -60,62 +87,150 @@ Example WebUI MCP config using `npx --yes` and `ADB_PATH` environment variable:
|
|
|
60
87
|
"args": [
|
|
61
88
|
"--yes",
|
|
62
89
|
"mobile-debug-mcp",
|
|
63
|
-
"server"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
"server"
|
|
91
|
+
],
|
|
92
|
+
"env": {
|
|
93
|
+
"ADB_PATH": "/path/to/adb",
|
|
94
|
+
"XCRUN_PATH": "/usr/bin/xcrun"
|
|
95
|
+
}
|
|
67
96
|
}
|
|
68
97
|
}
|
|
69
98
|
}
|
|
70
99
|
```
|
|
71
100
|
|
|
72
|
-
> Make sure to set
|
|
101
|
+
> Make sure to set `ADB_PATH` (Android) and `XCRUN_PATH` (iOS) if the tools are not in your system PATH.
|
|
73
102
|
|
|
74
103
|
---
|
|
75
104
|
|
|
76
105
|
## Tools
|
|
77
106
|
|
|
107
|
+
All tools accept a JSON input payload and return a structured JSON response. **Every response includes a `device` object** (with information about the selected device/simulator used for the operation), plus the tool-specific output.
|
|
108
|
+
|
|
78
109
|
### start_app
|
|
79
110
|
Launch a mobile app.
|
|
80
111
|
|
|
81
112
|
**Input:**
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"platform": "android" | "ios",
|
|
116
|
+
"appId": "com.example.app", // Android package or iOS bundle ID (Required)
|
|
117
|
+
"deviceId": "emulator-5554" // Optional: target specific device/simulator
|
|
118
|
+
}
|
|
119
|
+
```
|
|
82
120
|
|
|
121
|
+
**Response:**
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"device": { /* device info */ },
|
|
125
|
+
"appStarted": true,
|
|
126
|
+
"launchTimeMs": 123
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### get_logs
|
|
131
|
+
Fetch recent logs from the app or device.
|
|
132
|
+
|
|
133
|
+
**Input:**
|
|
83
134
|
```json
|
|
84
135
|
{
|
|
85
136
|
"platform": "android" | "ios",
|
|
86
|
-
"
|
|
137
|
+
"appId": "com.example.app", // Optional: filter logs by app
|
|
138
|
+
"deviceId": "emulator-5554", // Optional: target specific device
|
|
139
|
+
"lines": 200 // Optional: number of lines (Android only)
|
|
87
140
|
}
|
|
88
141
|
```
|
|
89
142
|
|
|
90
|
-
**
|
|
143
|
+
**Response:**
|
|
144
|
+
Returns two content blocks:
|
|
145
|
+
1. JSON metadata:
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"device": { /* device info */ },
|
|
149
|
+
"result": { "lines": 50, "crashLines": [...] }
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
2. Plain text log output.
|
|
91
153
|
|
|
154
|
+
### capture_screenshot
|
|
155
|
+
Capture a screenshot of the current device screen.
|
|
156
|
+
|
|
157
|
+
**Input:**
|
|
92
158
|
```json
|
|
93
159
|
{
|
|
94
|
-
"platform": "android",
|
|
95
|
-
"
|
|
160
|
+
"platform": "android" | "ios",
|
|
161
|
+
"deviceId": "emulator-5554" // Optional: target specific device
|
|
96
162
|
}
|
|
97
163
|
```
|
|
98
164
|
|
|
99
|
-
|
|
100
|
-
|
|
165
|
+
**Response:**
|
|
166
|
+
Returns two content blocks:
|
|
167
|
+
1. JSON metadata:
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"device": { /* device info */ },
|
|
171
|
+
"result": { "resolution": { "width": 1080, "height": 1920 } }
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
2. Image content (image/png) containing the raw PNG data.
|
|
175
|
+
|
|
176
|
+
### terminate_app
|
|
177
|
+
Terminate a running app.
|
|
101
178
|
|
|
102
179
|
**Input:**
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"platform": "android" | "ios",
|
|
183
|
+
"appId": "com.example.app", // Android package or iOS bundle ID (Required)
|
|
184
|
+
"deviceId": "emulator-5554" // Optional
|
|
185
|
+
}
|
|
186
|
+
```
|
|
103
187
|
|
|
188
|
+
**Response:**
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"device": { /* device info */ },
|
|
192
|
+
"appTerminated": true
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### restart_app
|
|
197
|
+
Restart an app (terminate then launch).
|
|
198
|
+
|
|
199
|
+
**Input:**
|
|
104
200
|
```json
|
|
105
201
|
{
|
|
106
202
|
"platform": "android" | "ios",
|
|
107
|
-
"
|
|
108
|
-
"
|
|
203
|
+
"appId": "com.example.app", // Android package or iOS bundle ID (Required)
|
|
204
|
+
"deviceId": "emulator-5554" // Optional
|
|
109
205
|
}
|
|
110
206
|
```
|
|
111
207
|
|
|
112
|
-
**
|
|
208
|
+
**Response:**
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"device": { /* device info */ },
|
|
212
|
+
"appRestarted": true,
|
|
213
|
+
"launchTimeMs": 123
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### reset_app_data
|
|
218
|
+
Clear app storage (reset to fresh install state).
|
|
219
|
+
|
|
220
|
+
**Input:**
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"platform": "android" | "ios",
|
|
224
|
+
"appId": "com.example.app", // Android package or iOS bundle ID (Required)
|
|
225
|
+
"deviceId": "emulator-5554" // Optional
|
|
226
|
+
}
|
|
227
|
+
```
|
|
113
228
|
|
|
229
|
+
**Response:**
|
|
114
230
|
```json
|
|
115
231
|
{
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"lines": 200
|
|
232
|
+
"device": { /* device info */ },
|
|
233
|
+
"dataCleared": true
|
|
119
234
|
}
|
|
120
235
|
```
|
|
121
236
|
|
|
@@ -126,15 +241,17 @@ Fetch recent logs from the app.
|
|
|
126
241
|
1. Ensure Android device or iOS simulator is running.
|
|
127
242
|
2. Use `start_app` to launch the app.
|
|
128
243
|
3. Use `get_logs` to read the latest logs.
|
|
129
|
-
4.
|
|
244
|
+
4. Use `capture_screenshot` to visually inspect the app if needed.
|
|
245
|
+
5. Use `reset_app_data` to clear state if debugging fresh install scenarios.
|
|
246
|
+
6. Use `restart_app` to quickly reboot the app during development cycles.
|
|
130
247
|
|
|
131
248
|
---
|
|
132
249
|
|
|
133
250
|
## Notes
|
|
134
251
|
|
|
135
|
-
- Ensure `adb` and `xcrun` are in your PATH or set `ADB_PATH` accordingly.
|
|
136
|
-
- For iOS, the simulator must be booted before using
|
|
137
|
-
-
|
|
252
|
+
- Ensure `adb` and `xcrun` are in your PATH or set `ADB_PATH` / `XCRUN_PATH` accordingly.
|
|
253
|
+
- For iOS, the simulator must be booted before using tools.
|
|
254
|
+
- The `capture_screenshot` tool returns a multi-block response: a JSON text block with metadata, followed by an image block containing the base64-encoded PNG data.
|
|
138
255
|
|
|
139
256
|
---
|
|
140
257
|
|
package/dist/android.js
CHANGED
|
@@ -1,44 +1,186 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
2
|
const ADB = process.env.ADB_PATH || "adb";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
resolve(stdout);
|
|
10
|
-
});
|
|
11
|
-
});
|
|
3
|
+
// Helper to construct ADB args with optional device ID
|
|
4
|
+
function getAdbArgs(args, deviceId) {
|
|
5
|
+
if (deviceId) {
|
|
6
|
+
return ['-s', deviceId, ...args];
|
|
7
|
+
}
|
|
8
|
+
return args;
|
|
12
9
|
}
|
|
13
|
-
|
|
10
|
+
function execAdb(args, deviceId, options = {}) {
|
|
11
|
+
const adbArgs = getAdbArgs(args, deviceId);
|
|
14
12
|
return new Promise((resolve, reject) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
// Use spawn instead of execFile for better stream control and to avoid potential buffering hangs
|
|
14
|
+
const child = spawn(ADB, adbArgs, options);
|
|
15
|
+
let stdout = '';
|
|
16
|
+
let stderr = '';
|
|
17
|
+
if (child.stdout) {
|
|
18
|
+
child.stdout.on('data', (data) => {
|
|
19
|
+
stdout += data.toString();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (child.stderr) {
|
|
23
|
+
child.stderr.on('data', (data) => {
|
|
24
|
+
stderr += data.toString();
|
|
26
25
|
});
|
|
26
|
+
}
|
|
27
|
+
const timeoutMs = args.includes('logcat') ? 10000 : 2000; // Shorter timeout for metadata queries
|
|
28
|
+
const timeout = setTimeout(() => {
|
|
29
|
+
child.kill();
|
|
30
|
+
reject(new Error(`ADB command timed out after ${timeoutMs}ms: ${args.join(' ')}`));
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
child.on('close', (code) => {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
if (code !== 0) {
|
|
35
|
+
// If there's an actual error (non-zero exit code), reject
|
|
36
|
+
reject(new Error(stderr.trim() || `Command failed with code ${code}`));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// If exit code is 0, resolve with stdout
|
|
40
|
+
resolve(stdout.trim());
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
child.on('error', (err) => {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
reject(err);
|
|
27
46
|
});
|
|
28
47
|
});
|
|
29
48
|
}
|
|
30
|
-
|
|
49
|
+
function getDeviceInfo(deviceId, metadata = {}) {
|
|
50
|
+
return {
|
|
51
|
+
platform: 'android',
|
|
52
|
+
id: deviceId || 'default',
|
|
53
|
+
osVersion: metadata.osVersion || '',
|
|
54
|
+
model: metadata.model || '',
|
|
55
|
+
simulator: metadata.simulator || false
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function getAndroidDeviceMetadata(appId, deviceId) {
|
|
31
59
|
try {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
// Run these in parallel to avoid sequential timeouts
|
|
61
|
+
const [osVersion, model, simOutput] = await Promise.all([
|
|
62
|
+
execAdb(['shell', 'getprop', 'ro.build.version.release'], deviceId).catch(() => ''),
|
|
63
|
+
execAdb(['shell', 'getprop', 'ro.product.model'], deviceId).catch(() => ''),
|
|
64
|
+
execAdb(['shell', 'getprop', 'ro.kernel.qemu'], deviceId).catch(() => '0')
|
|
65
|
+
]);
|
|
66
|
+
const simulator = simOutput === '1';
|
|
67
|
+
return { platform: 'android', id: deviceId || 'default', osVersion, model, simulator };
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return { platform: 'android', id: deviceId || 'default', osVersion: '', model: '', simulator: false };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function startAndroidApp(appId, deviceId) {
|
|
74
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
75
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
76
|
+
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
|
|
77
|
+
return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 };
|
|
78
|
+
}
|
|
79
|
+
export async function terminateAndroidApp(appId, deviceId) {
|
|
80
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
81
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
82
|
+
await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
|
|
83
|
+
return { device: deviceInfo, appTerminated: true };
|
|
84
|
+
}
|
|
85
|
+
export async function restartAndroidApp(appId, deviceId) {
|
|
86
|
+
await terminateAndroidApp(appId, deviceId);
|
|
87
|
+
const startResult = await startAndroidApp(appId, deviceId);
|
|
88
|
+
return {
|
|
89
|
+
device: startResult.device,
|
|
90
|
+
appRestarted: startResult.appStarted,
|
|
91
|
+
launchTimeMs: startResult.launchTimeMs
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function resetAndroidAppData(appId, deviceId) {
|
|
95
|
+
const metadata = await getAndroidDeviceMetadata(appId, deviceId);
|
|
96
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
97
|
+
const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
|
|
98
|
+
return { device: deviceInfo, dataCleared: output === 'Success' };
|
|
99
|
+
}
|
|
100
|
+
export async function getAndroidLogs(appId, lines = 200, deviceId) {
|
|
101
|
+
const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
|
|
102
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
103
|
+
try {
|
|
104
|
+
// We'll skip PID lookup for now to avoid potential hangs with 'pidof' on some emulators
|
|
105
|
+
// and rely on robust string matching against the log line.
|
|
106
|
+
// Get logs
|
|
107
|
+
const stdout = await execAdb(['logcat', '-d', '-t', lines.toString(), '-v', 'threadtime'], deviceId);
|
|
108
|
+
const allLogs = stdout.split('\n');
|
|
109
|
+
let filteredLogs = allLogs;
|
|
110
|
+
if (appId) {
|
|
111
|
+
// Filter by checking if the line contains the appId string.
|
|
112
|
+
const matchingLogs = allLogs.filter(line => line.includes(appId));
|
|
113
|
+
if (matchingLogs.length > 0) {
|
|
114
|
+
filteredLogs = matchingLogs;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Fallback: if no logs match the appId, return the raw logs (last N lines)
|
|
118
|
+
// This matches the behavior of the "working" version provided by the user,
|
|
119
|
+
// ensuring they at least see system activity if the app is silent or crashing early.
|
|
120
|
+
filteredLogs = allLogs;
|
|
121
|
+
}
|
|
38
122
|
}
|
|
39
|
-
return
|
|
123
|
+
return { device: deviceInfo, logs: filteredLogs, logCount: filteredLogs.length };
|
|
40
124
|
}
|
|
41
|
-
catch (
|
|
42
|
-
|
|
125
|
+
catch (e) {
|
|
126
|
+
console.error("Error fetching logs:", e);
|
|
127
|
+
return { device: deviceInfo, logs: [], logCount: 0 };
|
|
43
128
|
}
|
|
44
129
|
}
|
|
130
|
+
export async function captureAndroidScreen(deviceId) {
|
|
131
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
132
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const adbArgs = getAdbArgs(['exec-out', 'screencap', '-p'], deviceId);
|
|
135
|
+
// Using spawn for screencap as well to ensure consistent process handling
|
|
136
|
+
const child = spawn(ADB, adbArgs);
|
|
137
|
+
const chunks = [];
|
|
138
|
+
let stderr = '';
|
|
139
|
+
child.stdout.on('data', (chunk) => {
|
|
140
|
+
chunks.push(Buffer.from(chunk));
|
|
141
|
+
});
|
|
142
|
+
child.stderr.on('data', (data) => {
|
|
143
|
+
stderr += data.toString();
|
|
144
|
+
});
|
|
145
|
+
const timeout = setTimeout(() => {
|
|
146
|
+
child.kill();
|
|
147
|
+
reject(new Error(`ADB screencap timed out after 10s`));
|
|
148
|
+
}, 10000);
|
|
149
|
+
child.on('close', (code) => {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
if (code !== 0) {
|
|
152
|
+
reject(new Error(stderr.trim() || `Screencap failed with code ${code}`));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const screenshotBuffer = Buffer.concat(chunks);
|
|
156
|
+
const screenshotBase64 = screenshotBuffer.toString('base64');
|
|
157
|
+
// Get resolution
|
|
158
|
+
execAdb(['shell', 'wm', 'size'], deviceId)
|
|
159
|
+
.then(sizeStdout => {
|
|
160
|
+
let width = 0;
|
|
161
|
+
let height = 0;
|
|
162
|
+
const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/);
|
|
163
|
+
if (match) {
|
|
164
|
+
width = parseInt(match[1], 10);
|
|
165
|
+
height = parseInt(match[2], 10);
|
|
166
|
+
}
|
|
167
|
+
resolve({
|
|
168
|
+
device: deviceInfo,
|
|
169
|
+
screenshot: screenshotBase64,
|
|
170
|
+
resolution: { width, height }
|
|
171
|
+
});
|
|
172
|
+
})
|
|
173
|
+
.catch(() => {
|
|
174
|
+
resolve({
|
|
175
|
+
device: deviceInfo,
|
|
176
|
+
screenshot: screenshotBase64,
|
|
177
|
+
resolution: { width: 0, height: 0 }
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
child.on('error', (err) => {
|
|
182
|
+
clearTimeout(timeout);
|
|
183
|
+
reject(err);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|