mobile-debug-mcp 0.13.0 → 0.15.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/README.md +2 -2
- package/dist/android/interact.js +13 -1
- package/dist/android/observe.js +13 -0
- package/dist/cli/ios/run-ios-smoke.js +2 -2
- package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
- package/dist/interact/android.js +91 -0
- package/dist/interact/index.js +37 -0
- package/dist/interact/ios.js +120 -0
- package/dist/interact/shared/fingerprint.js +72 -0
- package/dist/interact/shared/scroll_to_element.js +98 -0
- package/dist/ios/interact.js +52 -1
- package/dist/ios/observe.js +12 -0
- package/dist/manage/android.js +162 -0
- package/dist/manage/index.js +364 -0
- package/dist/manage/ios.js +353 -0
- package/dist/observe/android.js +351 -0
- package/dist/observe/fingerprint.js +1 -0
- package/dist/observe/index.js +85 -0
- package/dist/observe/ios.js +320 -0
- package/dist/observe/test/device/logstream-real.js +34 -0
- package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
- package/dist/observe/test/device/run-scroll-test-android.js +22 -0
- package/dist/observe/test/device/test-ui-tree.js +67 -0
- package/dist/observe/test/device/wait_for_element_real.js +69 -0
- package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
- package/dist/observe/test/unit/logparse.test.js +39 -0
- package/dist/observe/test/unit/logstream.test.js +41 -0
- package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
- package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
- package/dist/server.js +54 -9
- package/dist/shared/fingerprint.js +72 -0
- package/dist/shared/scroll_to_element.js +98 -0
- package/dist/tools/interact.js +19 -22
- package/dist/tools/manage.js +2 -2
- package/dist/tools/observe.js +45 -43
- package/dist/tools/scroll_to_element.js +98 -0
- package/dist/utils/android/utils.js +429 -0
- package/dist/utils/cli/idb/check-idb.js +84 -0
- package/dist/utils/cli/idb/idb-helper.js +91 -0
- package/dist/utils/cli/idb/install-idb.js +82 -0
- package/dist/utils/cli/ios/preflight-ios.js +155 -0
- package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
- package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
- package/dist/utils/diagnostics.js +1 -1
- package/dist/utils/ios/utils.js +301 -0
- package/dist/utils/resolve-device.js +2 -2
- package/docs/CHANGELOG.md +11 -0
- package/docs/tools/TOOLS.md +3 -3
- package/docs/tools/interact.md +31 -0
- package/docs/tools/observe.md +24 -0
- package/package.json +1 -1
- package/src/{android/interact.ts → interact/android.ts} +15 -2
- package/src/interact/index.ts +47 -0
- package/src/{ios/interact.ts → interact/ios.ts} +58 -3
- package/src/interact/shared/fingerprint.ts +73 -0
- package/src/interact/shared/scroll_to_element.ts +110 -0
- package/src/{android/manage.ts → manage/android.ts} +2 -2
- package/src/{tools/manage.ts → manage/index.ts} +7 -4
- package/src/{ios/manage.ts → manage/ios.ts} +1 -1
- package/src/{android/observe.ts → observe/android.ts} +14 -26
- package/src/observe/index.ts +92 -0
- package/src/{ios/observe.ts → observe/ios.ts} +17 -35
- package/src/server.ts +57 -10
- package/src/{android → utils/android}/utils.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
- package/src/utils/diagnostics.ts +1 -1
- package/src/{ios → utils/ios}/utils.ts +2 -2
- package/src/utils/resolve-device.ts +2 -2
- package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
- package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
- package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
- package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
- package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
- package/test/observe/device/run-screen-fingerprint.ts +36 -0
- package/test/observe/device/run-scroll-test-android.ts +24 -0
- package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
- package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
- package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
- package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
- package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
- package/test/observe/unit/scroll_to_element.test.ts +129 -0
- package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +3 -3
- package/test/unit/index.ts +12 -11
- package/src/tools/interact.ts +0 -45
- package/src/tools/observe.ts +0 -82
- package/test/device/README.md +0 -49
- package/test/device/index.ts +0 -27
- package/test/device/utils/test-dist.ts +0 -41
- package/test/unit/utils/detect-java.test.ts +0 -22
- /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
- /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
- /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
- /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
- /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
- /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
- /package/test/{device/manage → manage/device}/run-build-install-ios.ts +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
import { getAdbCmd, execAdb, getAndroidDeviceMetadata, getDeviceInfo, delay, getScreenResolution, traverseNode, parseLogLine } from "../utils/android/utils.js";
|
|
4
|
+
import { createWriteStream } from "fs";
|
|
5
|
+
import { promises as fsPromises } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { computeScreenFingerprint } from "../interact/shared/fingerprint.js";
|
|
8
|
+
const activeLogStreams = new Map();
|
|
9
|
+
export class AndroidObserve {
|
|
10
|
+
async getDeviceMetadata(appId, deviceId) {
|
|
11
|
+
return getAndroidDeviceMetadata(appId, deviceId);
|
|
12
|
+
}
|
|
13
|
+
async getUITree(deviceId) {
|
|
14
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
15
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
16
|
+
try {
|
|
17
|
+
// Get screen resolution first
|
|
18
|
+
const resolution = await getScreenResolution(deviceId);
|
|
19
|
+
if (resolution.width === 0 && resolution.height === 0) {
|
|
20
|
+
throw new Error("Failed to get screen resolution. Is the device connected and authorized?");
|
|
21
|
+
}
|
|
22
|
+
// Retry Logic
|
|
23
|
+
let xmlContent = '';
|
|
24
|
+
let attempts = 0;
|
|
25
|
+
const maxAttempts = 3;
|
|
26
|
+
while (attempts < maxAttempts) {
|
|
27
|
+
attempts++;
|
|
28
|
+
try {
|
|
29
|
+
// Stabilization delay
|
|
30
|
+
await delay(300 + (attempts * 100)); // 300ms, 400ms, 500ms...
|
|
31
|
+
// Dump UI hierarchy
|
|
32
|
+
await execAdb(['shell', 'uiautomator', 'dump', '/sdcard/ui.xml'], deviceId);
|
|
33
|
+
// Read the file
|
|
34
|
+
xmlContent = await execAdb(['shell', 'cat', '/sdcard/ui.xml'], deviceId);
|
|
35
|
+
// Check validity
|
|
36
|
+
if (xmlContent && xmlContent.trim().length > 0 && !xmlContent.includes("ERROR:")) {
|
|
37
|
+
break; // Success
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error(`Attempt ${attempts} failed: ${e}`);
|
|
42
|
+
}
|
|
43
|
+
if (attempts === maxAttempts) {
|
|
44
|
+
throw new Error(`Failed to retrieve valid UI dump after ${maxAttempts} attempts.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const parser = new XMLParser({
|
|
48
|
+
ignoreAttributes: false,
|
|
49
|
+
attributeNamePrefix: "@_"
|
|
50
|
+
});
|
|
51
|
+
const result = parser.parse(xmlContent);
|
|
52
|
+
const elements = [];
|
|
53
|
+
// The root is usually hierarchy -> node
|
|
54
|
+
if (result.hierarchy && result.hierarchy.node) {
|
|
55
|
+
// If the root is an array (unlikely for root, but good to be safe) or single object
|
|
56
|
+
if (Array.isArray(result.hierarchy.node)) {
|
|
57
|
+
result.hierarchy.node.forEach((n) => traverseNode(n, elements));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
traverseNode(result.hierarchy.node, elements);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
device: deviceInfo,
|
|
65
|
+
screen: "",
|
|
66
|
+
resolution,
|
|
67
|
+
elements
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
const errorMessage = `Failed to get UI tree. ADB Path: '${getAdbCmd()}'. Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
72
|
+
console.error(errorMessage);
|
|
73
|
+
return {
|
|
74
|
+
device: deviceInfo,
|
|
75
|
+
screen: "",
|
|
76
|
+
resolution: { width: 0, height: 0 },
|
|
77
|
+
elements: [],
|
|
78
|
+
error: errorMessage
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async getLogs(appId, lines = 200, deviceId) {
|
|
83
|
+
const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
|
|
84
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
85
|
+
try {
|
|
86
|
+
const stdout = await execAdb(['logcat', '-d', '-t', lines.toString(), '-v', 'threadtime'], deviceId);
|
|
87
|
+
const allLogs = stdout.split('\n');
|
|
88
|
+
let filteredLogs = allLogs;
|
|
89
|
+
if (appId) {
|
|
90
|
+
const matchingLogs = allLogs.filter(line => line.includes(appId));
|
|
91
|
+
if (matchingLogs.length > 0) {
|
|
92
|
+
filteredLogs = matchingLogs;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
filteredLogs = allLogs;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { device: deviceInfo, logs: filteredLogs, logCount: filteredLogs.length };
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
console.error("Error fetching logs:", e);
|
|
102
|
+
return { device: deviceInfo, logs: [], logCount: 0 };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async captureScreen(deviceId) {
|
|
106
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
107
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const args = deviceId ? ['-s', deviceId, 'exec-out', 'screencap', '-p'] : ['exec-out', 'screencap', '-p'];
|
|
110
|
+
const child = spawn(getAdbCmd(), args);
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let stderr = '';
|
|
113
|
+
child.stdout.on('data', (chunk) => {
|
|
114
|
+
chunks.push(Buffer.from(chunk));
|
|
115
|
+
});
|
|
116
|
+
child.stderr.on('data', (data) => {
|
|
117
|
+
stderr += data.toString();
|
|
118
|
+
});
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
child.kill();
|
|
121
|
+
reject(new Error(`ADB screencap timed out after 10s`));
|
|
122
|
+
}, 10000);
|
|
123
|
+
child.on('close', (code) => {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
if (code !== 0) {
|
|
126
|
+
reject(new Error(stderr.trim() || `Screencap failed with code ${code}`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const screenshotBuffer = Buffer.concat(chunks);
|
|
130
|
+
const screenshotBase64 = screenshotBuffer.toString('base64');
|
|
131
|
+
execAdb(['shell', 'wm', 'size'], deviceId)
|
|
132
|
+
.then(sizeStdout => {
|
|
133
|
+
let width = 0;
|
|
134
|
+
let height = 0;
|
|
135
|
+
const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/);
|
|
136
|
+
if (match) {
|
|
137
|
+
width = parseInt(match[1], 10);
|
|
138
|
+
height = parseInt(match[2], 10);
|
|
139
|
+
}
|
|
140
|
+
resolve({
|
|
141
|
+
device: deviceInfo,
|
|
142
|
+
screenshot: screenshotBase64,
|
|
143
|
+
resolution: { width, height }
|
|
144
|
+
});
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {
|
|
147
|
+
resolve({
|
|
148
|
+
device: deviceInfo,
|
|
149
|
+
screenshot: screenshotBase64,
|
|
150
|
+
resolution: { width: 0, height: 0 }
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
child.on('error', (err) => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
reject(err);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async getCurrentScreen(deviceId) {
|
|
161
|
+
const metadata = await getAndroidDeviceMetadata("", deviceId);
|
|
162
|
+
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
|
|
163
|
+
try {
|
|
164
|
+
const output = await execAdb(['shell', 'dumpsys', 'activity', 'activities'], deviceId, { timeout: 10000 });
|
|
165
|
+
const lines = output.split('\n');
|
|
166
|
+
let resumedLine = lines.find(line => /^\s*mResumedActivity:/.test(line));
|
|
167
|
+
if (!resumedLine) {
|
|
168
|
+
resumedLine = lines.find(line => /^\s*ResumedActivity:/.test(line));
|
|
169
|
+
}
|
|
170
|
+
if (!resumedLine) {
|
|
171
|
+
return {
|
|
172
|
+
device: deviceInfo,
|
|
173
|
+
package: "",
|
|
174
|
+
activity: "",
|
|
175
|
+
shortActivity: "",
|
|
176
|
+
error: "Could not find 'mResumedActivity' in dumpsys output"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const match = resumedLine.match(/ActivityRecord\{[^ ]*(?:\s+[^ ]+)*\s+([^\/ ]+)\/([^ \{}]+)[^}]*\}/);
|
|
180
|
+
if (match) {
|
|
181
|
+
const packageName = match[1];
|
|
182
|
+
let activityName = match[2];
|
|
183
|
+
if (activityName.startsWith('.')) {
|
|
184
|
+
activityName = packageName + activityName;
|
|
185
|
+
}
|
|
186
|
+
const shortActivity = activityName.split('.').pop() || activityName;
|
|
187
|
+
return {
|
|
188
|
+
device: deviceInfo,
|
|
189
|
+
package: packageName,
|
|
190
|
+
activity: activityName,
|
|
191
|
+
shortActivity: shortActivity
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
return {
|
|
196
|
+
device: deviceInfo,
|
|
197
|
+
package: "",
|
|
198
|
+
activity: "",
|
|
199
|
+
shortActivity: "",
|
|
200
|
+
error: `Found resumed activity line but failed to parse: '${resumedLine.trim()}'`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
return {
|
|
206
|
+
device: deviceInfo,
|
|
207
|
+
package: "",
|
|
208
|
+
activity: "",
|
|
209
|
+
shortActivity: "",
|
|
210
|
+
error: e instanceof Error ? e.message : String(e)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async getScreenFingerprint(deviceId) {
|
|
215
|
+
try {
|
|
216
|
+
const tree = await this.getUITree(deviceId);
|
|
217
|
+
if (!tree || tree.error)
|
|
218
|
+
return { fingerprint: null, error: tree.error };
|
|
219
|
+
const current = await this.getCurrentScreen(deviceId).catch(() => null);
|
|
220
|
+
return computeScreenFingerprint(tree, current, 'android', 50);
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
return { fingerprint: null, error: e instanceof Error ? e.message : String(e) };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async startLogStream(packageName, level = 'error', deviceId, sessionId = 'default') {
|
|
227
|
+
try {
|
|
228
|
+
const pidOutput = await execAdb(['shell', 'pidof', packageName], deviceId).catch(() => '');
|
|
229
|
+
const pid = (pidOutput || '').trim();
|
|
230
|
+
if (!pid)
|
|
231
|
+
return { success: false, error: 'app_not_running' };
|
|
232
|
+
const levelMap = { error: '*:E', warn: '*:W', info: '*:I', debug: '*:D' };
|
|
233
|
+
const filter = levelMap[level] || levelMap['error'];
|
|
234
|
+
if (activeLogStreams.has(sessionId)) {
|
|
235
|
+
try {
|
|
236
|
+
activeLogStreams.get(sessionId).proc.kill();
|
|
237
|
+
}
|
|
238
|
+
catch { }
|
|
239
|
+
activeLogStreams.delete(sessionId);
|
|
240
|
+
}
|
|
241
|
+
const args = ['logcat', `--pid=${pid}`, filter];
|
|
242
|
+
const proc = spawn(getAdbCmd(), args);
|
|
243
|
+
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
244
|
+
const file = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
|
|
245
|
+
const stream = createWriteStream(file, { flags: 'a' });
|
|
246
|
+
proc.stdout.on('data', (chunk) => {
|
|
247
|
+
const text = chunk.toString();
|
|
248
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
249
|
+
for (const l of lines) {
|
|
250
|
+
const entry = parseLogLine(l);
|
|
251
|
+
stream.write(JSON.stringify(entry) + '\n');
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
proc.stderr.on('data', (chunk) => {
|
|
255
|
+
const text = chunk.toString();
|
|
256
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
257
|
+
for (const l of lines) {
|
|
258
|
+
const entry = { timestamp: '', level: 'E', tag: 'adb', message: l };
|
|
259
|
+
stream.write(JSON.stringify(entry) + '\n');
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
proc.on('close', () => {
|
|
263
|
+
stream.end();
|
|
264
|
+
activeLogStreams.delete(sessionId);
|
|
265
|
+
});
|
|
266
|
+
activeLogStreams.set(sessionId, { proc, file });
|
|
267
|
+
return { success: true, stream_started: true };
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async stopLogStream(sessionId = 'default') {
|
|
274
|
+
const entry = activeLogStreams.get(sessionId);
|
|
275
|
+
if (!entry)
|
|
276
|
+
return { success: true };
|
|
277
|
+
try {
|
|
278
|
+
entry.proc.kill();
|
|
279
|
+
}
|
|
280
|
+
catch { }
|
|
281
|
+
activeLogStreams.delete(sessionId);
|
|
282
|
+
return { success: true };
|
|
283
|
+
}
|
|
284
|
+
async readLogStream(sessionId = 'default', limit = 100, since) {
|
|
285
|
+
const entry = activeLogStreams.get(sessionId);
|
|
286
|
+
let file;
|
|
287
|
+
if (entry && entry.file)
|
|
288
|
+
file = entry.file;
|
|
289
|
+
else {
|
|
290
|
+
const tmpDir = process.env.TMPDIR || '/tmp';
|
|
291
|
+
const candidate = path.join(tmpDir, `mobile-debug-log-${sessionId}.ndjson`);
|
|
292
|
+
file = candidate;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const data = await fsPromises.readFile(file, 'utf8').catch(() => '');
|
|
296
|
+
if (!data)
|
|
297
|
+
return { entries: [], crash_summary: { crash_detected: false } };
|
|
298
|
+
const lines = data.split(/\r?\n/).filter(Boolean);
|
|
299
|
+
const parsed = lines.map(l => {
|
|
300
|
+
try {
|
|
301
|
+
const obj = JSON.parse(l);
|
|
302
|
+
if (typeof obj._iso === 'undefined') {
|
|
303
|
+
let iso = null;
|
|
304
|
+
if (obj.timestamp) {
|
|
305
|
+
const d = new Date(obj.timestamp);
|
|
306
|
+
if (!isNaN(d.getTime()))
|
|
307
|
+
iso = d.toISOString();
|
|
308
|
+
}
|
|
309
|
+
obj._iso = iso;
|
|
310
|
+
}
|
|
311
|
+
if (typeof obj.crash === 'undefined') {
|
|
312
|
+
const msg = (obj.message || '').toString();
|
|
313
|
+
const exMatch = msg.match(/\b([A-Za-z0-9_$.]+Exception)\b/);
|
|
314
|
+
if (/FATAL EXCEPTION/i.test(msg) || exMatch) {
|
|
315
|
+
obj.crash = true;
|
|
316
|
+
if (exMatch)
|
|
317
|
+
obj.exception = exMatch[1];
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
obj.crash = false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return obj;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return { message: l, _iso: null, crash: false };
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
let filtered = parsed;
|
|
330
|
+
if (since) {
|
|
331
|
+
let sinceMs = null;
|
|
332
|
+
if (/^\d+$/.test(since))
|
|
333
|
+
sinceMs = Number(since);
|
|
334
|
+
else {
|
|
335
|
+
const sDate = new Date(since);
|
|
336
|
+
if (!isNaN(sDate.getTime()))
|
|
337
|
+
sinceMs = sDate.getTime();
|
|
338
|
+
}
|
|
339
|
+
if (sinceMs !== null)
|
|
340
|
+
filtered = parsed.filter(p => p._iso && (new Date(p._iso).getTime() >= sinceMs));
|
|
341
|
+
}
|
|
342
|
+
const entries = filtered.slice(-Math.max(0, limit));
|
|
343
|
+
const crashEntry = entries.find(e => e.crash);
|
|
344
|
+
const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
|
|
345
|
+
return { entries, crash_summary };
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return { entries: [], crash_summary: { crash_detected: false } };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../intera../interact/shared/fingerprint.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolveTargetDevice } from '../utils/resolve-device.js';
|
|
2
|
+
import { AndroidObserve } from './android.js';
|
|
3
|
+
import { iOSObserve } from './ios.js';
|
|
4
|
+
export { AndroidObserve } from './android.js';
|
|
5
|
+
export { iOSObserve } from './ios.js';
|
|
6
|
+
export class ToolsObserve {
|
|
7
|
+
// Resolve a target device and return the appropriate observe instance and resolved info.
|
|
8
|
+
static async resolveObserve(platform, deviceId, appId) {
|
|
9
|
+
if (platform === 'android') {
|
|
10
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId, appId });
|
|
11
|
+
return { observe: new AndroidObserve(), resolved };
|
|
12
|
+
}
|
|
13
|
+
if (platform === 'ios') {
|
|
14
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId, appId });
|
|
15
|
+
return { observe: new iOSObserve(), resolved };
|
|
16
|
+
}
|
|
17
|
+
// No platform specified: try android then ios
|
|
18
|
+
try {
|
|
19
|
+
const resolved = await resolveTargetDevice({ platform: 'android', deviceId, appId });
|
|
20
|
+
return { observe: new AndroidObserve(), resolved };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId, appId });
|
|
24
|
+
return { observe: new iOSObserve(), resolved };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
static async getUITreeHandler({ platform, deviceId }) {
|
|
28
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId);
|
|
29
|
+
return await observe.getUITree(resolved.id);
|
|
30
|
+
}
|
|
31
|
+
static async getCurrentScreenHandler({ deviceId }) {
|
|
32
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve('android', deviceId);
|
|
33
|
+
// getCurrentScreen is Android-specific
|
|
34
|
+
return await observe.getCurrentScreen(resolved.id);
|
|
35
|
+
}
|
|
36
|
+
static async getLogsHandler({ platform, appId, deviceId, lines }) {
|
|
37
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId, appId);
|
|
38
|
+
if (observe instanceof AndroidObserve) {
|
|
39
|
+
const response = await observe.getLogs(appId, lines ?? 200, resolved.id);
|
|
40
|
+
const logs = Array.isArray(response.logs) ? response.logs : [];
|
|
41
|
+
const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'));
|
|
42
|
+
return { device: response.device, logs, crashLines };
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const resp = await observe.getLogs(appId, resolved.id);
|
|
46
|
+
const logs = Array.isArray(resp.logs) ? resp.logs : [];
|
|
47
|
+
const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'));
|
|
48
|
+
return { device: resp.device, logs, crashLines };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
static async startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }) {
|
|
52
|
+
const sid = sessionId || 'default';
|
|
53
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId, packageName);
|
|
54
|
+
if (observe instanceof AndroidObserve) {
|
|
55
|
+
return await observe.startLogStream(packageName, level || 'error', resolved.id, sid);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return await observe.startLogStream(packageName, resolved.id, sid);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
static async readLogStreamHandler({ platform, sessionId, limit, since }) {
|
|
62
|
+
const sid = sessionId || 'default';
|
|
63
|
+
const { observe } = await ToolsObserve.resolveObserve(platform);
|
|
64
|
+
return await observe.readLogStream(sid, limit ?? 100, since);
|
|
65
|
+
}
|
|
66
|
+
static async stopLogStreamHandler({ platform, sessionId }) {
|
|
67
|
+
const sid = sessionId || 'default';
|
|
68
|
+
const { observe } = await ToolsObserve.resolveObserve(platform);
|
|
69
|
+
return await observe.stopLogStream(sid);
|
|
70
|
+
}
|
|
71
|
+
static async captureScreenshotHandler({ platform, deviceId }) {
|
|
72
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId);
|
|
73
|
+
if (observe instanceof AndroidObserve) {
|
|
74
|
+
return await observe.captureScreen(resolved.id);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return await observe.captureScreenshot(resolved.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
static async getScreenFingerprintHandler({ platform, deviceId } = {}) {
|
|
81
|
+
const { observe, resolved } = await ToolsObserve.resolveObserve(platform, deviceId);
|
|
82
|
+
// Both observes implement getScreenFingerprint
|
|
83
|
+
return await observe.getScreenFingerprint(resolved.id);
|
|
84
|
+
}
|
|
85
|
+
}
|