mobile-debug-mcp 0.4.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 +154 -23
- package/dist/android.js +174 -32
- package/dist/ios.js +203 -14
- package/dist/server.js +263 -20
- package/docs/CHANGELOG.md +23 -0
- package/package.json +2 -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 +305 -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
|
|
|
@@ -25,7 +32,9 @@
|
|
|
25
32
|
|
|
26
33
|
## Installation
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
You can install and use **Mobile Debug MCP** in one of two ways:
|
|
36
|
+
|
|
37
|
+
### 1. Clone the repository for local development
|
|
29
38
|
|
|
30
39
|
```bash
|
|
31
40
|
git clone https://github.com/YOUR_USERNAME/mobile-debug-mcp.git
|
|
@@ -34,74 +43,194 @@ npm install
|
|
|
34
43
|
npm run build
|
|
35
44
|
```
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
This option is suitable if you want to modify or contribute to the code.
|
|
47
|
+
|
|
48
|
+
### 2. Install via npm for standard use
|
|
38
49
|
|
|
39
50
|
```bash
|
|
40
51
|
npm install -g mobile-debug-mcp
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
This option installs the package globally for easy use without cloning the repo.
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
---
|
|
46
57
|
|
|
47
|
-
|
|
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:
|
|
48
81
|
|
|
49
82
|
```json
|
|
50
83
|
{
|
|
51
84
|
"mcpServers": {
|
|
52
85
|
"mobile-debug": {
|
|
53
|
-
"command": "
|
|
54
|
-
"args": [
|
|
86
|
+
"command": "npx",
|
|
87
|
+
"args": [
|
|
88
|
+
"--yes",
|
|
89
|
+
"mobile-debug-mcp",
|
|
90
|
+
"server"
|
|
91
|
+
],
|
|
92
|
+
"env": {
|
|
93
|
+
"ADB_PATH": "/path/to/adb",
|
|
94
|
+
"XCRUN_PATH": "/usr/bin/xcrun"
|
|
95
|
+
}
|
|
55
96
|
}
|
|
56
97
|
}
|
|
57
98
|
}
|
|
58
99
|
```
|
|
59
100
|
|
|
60
|
-
> Make sure to
|
|
101
|
+
> Make sure to set `ADB_PATH` (Android) and `XCRUN_PATH` (iOS) if the tools are not in your system PATH.
|
|
61
102
|
|
|
62
103
|
---
|
|
63
104
|
|
|
64
105
|
## Tools
|
|
65
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
|
+
|
|
66
109
|
### start_app
|
|
67
110
|
Launch a mobile app.
|
|
68
111
|
|
|
69
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
|
+
```
|
|
70
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:**
|
|
71
134
|
```json
|
|
72
135
|
{
|
|
73
136
|
"platform": "android" | "ios",
|
|
74
|
-
"
|
|
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)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Response:**
|
|
144
|
+
Returns two content blocks:
|
|
145
|
+
1. JSON metadata:
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"device": { /* device info */ },
|
|
149
|
+
"result": { "lines": 50, "crashLines": [...] }
|
|
75
150
|
}
|
|
76
151
|
```
|
|
152
|
+
2. Plain text log output.
|
|
77
153
|
|
|
78
|
-
|
|
154
|
+
### capture_screenshot
|
|
155
|
+
Capture a screenshot of the current device screen.
|
|
79
156
|
|
|
157
|
+
**Input:**
|
|
80
158
|
```json
|
|
81
159
|
{
|
|
82
|
-
"platform": "android",
|
|
83
|
-
"
|
|
160
|
+
"platform": "android" | "ios",
|
|
161
|
+
"deviceId": "emulator-5554" // Optional: target specific device
|
|
84
162
|
}
|
|
85
163
|
```
|
|
86
164
|
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
89
178
|
|
|
90
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
|
+
```
|
|
187
|
+
|
|
188
|
+
**Response:**
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"device": { /* device info */ },
|
|
192
|
+
"appTerminated": true
|
|
193
|
+
}
|
|
194
|
+
```
|
|
91
195
|
|
|
196
|
+
### restart_app
|
|
197
|
+
Restart an app (terminate then launch).
|
|
198
|
+
|
|
199
|
+
**Input:**
|
|
92
200
|
```json
|
|
93
201
|
{
|
|
94
202
|
"platform": "android" | "ios",
|
|
95
|
-
"
|
|
203
|
+
"appId": "com.example.app", // Android package or iOS bundle ID (Required)
|
|
204
|
+
"deviceId": "emulator-5554" // Optional
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Response:**
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"device": { /* device info */ },
|
|
212
|
+
"appRestarted": true,
|
|
213
|
+
"launchTimeMs": 123
|
|
96
214
|
}
|
|
97
215
|
```
|
|
98
216
|
|
|
99
|
-
|
|
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
|
+
```
|
|
100
228
|
|
|
229
|
+
**Response:**
|
|
101
230
|
```json
|
|
102
231
|
{
|
|
103
|
-
"
|
|
104
|
-
"
|
|
232
|
+
"device": { /* device info */ },
|
|
233
|
+
"dataCleared": true
|
|
105
234
|
}
|
|
106
235
|
```
|
|
107
236
|
|
|
@@ -112,15 +241,17 @@ Fetch recent logs from the app.
|
|
|
112
241
|
1. Ensure Android device or iOS simulator is running.
|
|
113
242
|
2. Use `start_app` to launch the app.
|
|
114
243
|
3. Use `get_logs` to read the latest logs.
|
|
115
|
-
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.
|
|
116
247
|
|
|
117
248
|
---
|
|
118
249
|
|
|
119
250
|
## Notes
|
|
120
251
|
|
|
121
|
-
- Ensure `adb` and `xcrun` are in your PATH.
|
|
122
|
-
- For iOS, the simulator must be booted before using
|
|
123
|
-
-
|
|
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.
|
|
124
255
|
|
|
125
256
|
---
|
|
126
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
|
+
}
|