@midscene/android 1.5.3-beta-20260309062917.0 → 1.5.3
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/dist/es/cli.mjs +57 -11
- package/dist/es/index.mjs +57 -11
- package/dist/es/mcp-server.mjs +57 -11
- package/dist/lib/cli.js +57 -11
- package/dist/lib/index.js +57 -11
- package/dist/lib/mcp-server.js +57 -11
- package/dist/types/index.d.ts +4 -0
- package/dist/types/mcp-server.d.ts +2 -0
- package/package.json +4 -4
package/dist/es/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { BaseMidsceneTools } from "@midscene/shared/mcp";
|
|
|
8
8
|
import { Agent } from "@midscene/core/agent";
|
|
9
9
|
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
|
|
10
10
|
import node_assert from "node:assert";
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
11
12
|
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
|
|
12
13
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
13
14
|
import { MIDSCENE_ADB_PATH, MIDSCENE_ADB_REMOTE_HOST, MIDSCENE_ADB_REMOTE_PORT, MIDSCENE_ANDROID_IME_STRATEGY, globalConfigManager } from "@midscene/shared/env";
|
|
@@ -50,6 +51,10 @@ var __webpack_modules__ = {
|
|
|
50
51
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
51
52
|
const MAX_SCAN_BYTES = 1000;
|
|
52
53
|
const CONNECTION_WAIT_MS = 1000;
|
|
54
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
55
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
56
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
57
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
53
58
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
54
59
|
enabled: false,
|
|
55
60
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -147,16 +152,37 @@ var __webpack_modules__ = {
|
|
|
147
152
|
this.consumeFramesLoop(reader);
|
|
148
153
|
}
|
|
149
154
|
async consumeFramesLoop(reader) {
|
|
155
|
+
let readCount = 0;
|
|
156
|
+
let windowStart = Date.now();
|
|
157
|
+
let lastBusyWarn = 0;
|
|
158
|
+
let totalReads = 0;
|
|
150
159
|
try {
|
|
151
160
|
while(true){
|
|
152
161
|
const { done, value } = await reader.read();
|
|
153
162
|
if (done) break;
|
|
163
|
+
totalReads++;
|
|
164
|
+
readCount++;
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const elapsed = now - windowStart;
|
|
167
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
168
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
169
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
170
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
171
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
172
|
+
lastBusyWarn = now;
|
|
173
|
+
}
|
|
174
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
175
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
176
|
+
readCount = 0;
|
|
177
|
+
windowStart = Date.now();
|
|
178
|
+
}
|
|
154
179
|
this.processFrame(value);
|
|
155
180
|
}
|
|
156
181
|
} catch (error) {
|
|
157
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
182
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
158
183
|
await this.disconnect();
|
|
159
184
|
}
|
|
185
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
160
186
|
}
|
|
161
187
|
processFrame(packet) {
|
|
162
188
|
if ('configuration' === packet.type) {
|
|
@@ -1171,14 +1197,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1171
1197
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1172
1198
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1173
1199
|
try {
|
|
1174
|
-
if (useShellScreencap
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1200
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1201
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1202
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1203
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1204
|
+
if (!screenshotBuffer) {
|
|
1205
|
+
this.takeScreenshotFailCount++;
|
|
1206
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1207
|
+
}
|
|
1208
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1209
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1210
|
+
this.takeScreenshotFailCount++;
|
|
1211
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1212
|
+
}
|
|
1213
|
+
this.takeScreenshotFailCount = 0;
|
|
1214
|
+
} else {
|
|
1215
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1216
|
+
throw new Error('Using shell screencap directly');
|
|
1182
1217
|
}
|
|
1183
1218
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1184
1219
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1210,9 +1245,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1210
1245
|
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1211
1246
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1212
1247
|
} finally{
|
|
1213
|
-
|
|
1214
|
-
|
|
1248
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1249
|
+
const child = execFile(adbPath, [
|
|
1250
|
+
'-s',
|
|
1251
|
+
this.deviceId,
|
|
1252
|
+
'shell',
|
|
1253
|
+
`rm ${androidScreenshotPath}`
|
|
1254
|
+
], {
|
|
1255
|
+
timeout: 3000
|
|
1256
|
+
}, (err)=>{
|
|
1257
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1215
1258
|
});
|
|
1259
|
+
child.unref();
|
|
1216
1260
|
}
|
|
1217
1261
|
}
|
|
1218
1262
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1656,6 +1700,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1656
1700
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1657
1701
|
device_define_property(this, "appNameMapping", {});
|
|
1658
1702
|
device_define_property(this, "cachedAdjustScale", null);
|
|
1703
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1659
1704
|
device_define_property(this, "interfaceType", 'android');
|
|
1660
1705
|
device_define_property(this, "uri", void 0);
|
|
1661
1706
|
device_define_property(this, "options", void 0);
|
|
@@ -1665,6 +1710,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1665
1710
|
this.customActions = options?.customActions;
|
|
1666
1711
|
}
|
|
1667
1712
|
}
|
|
1713
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1668
1714
|
const runAdbShellParamSchema = z.object({
|
|
1669
1715
|
command: z.string().describe('ADB shell command to execute')
|
|
1670
1716
|
});
|
package/dist/es/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
|
3
3
|
import * as __rspack_external_node_module_ab9f2194 from "node:module";
|
|
4
4
|
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
5
5
|
import node_assert from "node:assert";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
6
7
|
import { getMidsceneLocationSchema, z } from "@midscene/core";
|
|
7
8
|
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
|
|
8
9
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
@@ -48,6 +49,10 @@ var __webpack_modules__ = {
|
|
|
48
49
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
49
50
|
const MAX_SCAN_BYTES = 1000;
|
|
50
51
|
const CONNECTION_WAIT_MS = 1000;
|
|
52
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
53
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
54
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
55
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
51
56
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
52
57
|
enabled: false,
|
|
53
58
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -145,16 +150,37 @@ var __webpack_modules__ = {
|
|
|
145
150
|
this.consumeFramesLoop(reader);
|
|
146
151
|
}
|
|
147
152
|
async consumeFramesLoop(reader) {
|
|
153
|
+
let readCount = 0;
|
|
154
|
+
let windowStart = Date.now();
|
|
155
|
+
let lastBusyWarn = 0;
|
|
156
|
+
let totalReads = 0;
|
|
148
157
|
try {
|
|
149
158
|
while(true){
|
|
150
159
|
const { done, value } = await reader.read();
|
|
151
160
|
if (done) break;
|
|
161
|
+
totalReads++;
|
|
162
|
+
readCount++;
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const elapsed = now - windowStart;
|
|
165
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
166
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
167
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
168
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
169
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
170
|
+
lastBusyWarn = now;
|
|
171
|
+
}
|
|
172
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
173
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
174
|
+
readCount = 0;
|
|
175
|
+
windowStart = Date.now();
|
|
176
|
+
}
|
|
152
177
|
this.processFrame(value);
|
|
153
178
|
}
|
|
154
179
|
} catch (error) {
|
|
155
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
180
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
156
181
|
await this.disconnect();
|
|
157
182
|
}
|
|
183
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
158
184
|
}
|
|
159
185
|
processFrame(packet) {
|
|
160
186
|
if ('configuration' === packet.type) {
|
|
@@ -1073,14 +1099,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1073
1099
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1074
1100
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1075
1101
|
try {
|
|
1076
|
-
if (useShellScreencap
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1102
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1103
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1104
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1105
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1106
|
+
if (!screenshotBuffer) {
|
|
1107
|
+
this.takeScreenshotFailCount++;
|
|
1108
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1109
|
+
}
|
|
1110
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1111
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1112
|
+
this.takeScreenshotFailCount++;
|
|
1113
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1114
|
+
}
|
|
1115
|
+
this.takeScreenshotFailCount = 0;
|
|
1116
|
+
} else {
|
|
1117
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1118
|
+
throw new Error('Using shell screencap directly');
|
|
1084
1119
|
}
|
|
1085
1120
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1086
1121
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1112,9 +1147,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1112
1147
|
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1113
1148
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1114
1149
|
} finally{
|
|
1115
|
-
|
|
1116
|
-
|
|
1150
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1151
|
+
const child = execFile(adbPath, [
|
|
1152
|
+
'-s',
|
|
1153
|
+
this.deviceId,
|
|
1154
|
+
'shell',
|
|
1155
|
+
`rm ${androidScreenshotPath}`
|
|
1156
|
+
], {
|
|
1157
|
+
timeout: 3000
|
|
1158
|
+
}, (err)=>{
|
|
1159
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1117
1160
|
});
|
|
1161
|
+
child.unref();
|
|
1118
1162
|
}
|
|
1119
1163
|
}
|
|
1120
1164
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1558,6 +1602,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1558
1602
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1559
1603
|
device_define_property(this, "appNameMapping", {});
|
|
1560
1604
|
device_define_property(this, "cachedAdjustScale", null);
|
|
1605
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1561
1606
|
device_define_property(this, "interfaceType", 'android');
|
|
1562
1607
|
device_define_property(this, "uri", void 0);
|
|
1563
1608
|
device_define_property(this, "options", void 0);
|
|
@@ -1567,6 +1612,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1567
1612
|
this.customActions = options?.customActions;
|
|
1568
1613
|
}
|
|
1569
1614
|
}
|
|
1615
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1570
1616
|
const runAdbShellParamSchema = z.object({
|
|
1571
1617
|
command: z.string().describe('ADB shell command to execute')
|
|
1572
1618
|
});
|
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { BaseMCPServer, BaseMidsceneTools, createMCPServerLauncher } from "@mids
|
|
|
6
6
|
import { Agent } from "@midscene/core/agent";
|
|
7
7
|
import { mergeAndNormalizeAppNameMapping, normalizeForComparison, repeat } from "@midscene/shared/utils";
|
|
8
8
|
import node_assert from "node:assert";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
9
10
|
import { getMidsceneLocationSchema, z } from "@midscene/core";
|
|
10
11
|
import { defineAction, defineActionClearInput, defineActionCursorMove, defineActionDoubleClick, defineActionDragAndDrop, defineActionKeyboardPress, defineActionScroll, defineActionSwipe, defineActionTap, normalizeMobileSwipeParam } from "@midscene/core/device";
|
|
11
12
|
import { getTmpFile, sleep } from "@midscene/core/utils";
|
|
@@ -49,6 +50,10 @@ var __webpack_modules__ = {
|
|
|
49
50
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
50
51
|
const MAX_SCAN_BYTES = 1000;
|
|
51
52
|
const CONNECTION_WAIT_MS = 1000;
|
|
53
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
54
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
55
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
56
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
52
57
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
53
58
|
enabled: false,
|
|
54
59
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -146,16 +151,37 @@ var __webpack_modules__ = {
|
|
|
146
151
|
this.consumeFramesLoop(reader);
|
|
147
152
|
}
|
|
148
153
|
async consumeFramesLoop(reader) {
|
|
154
|
+
let readCount = 0;
|
|
155
|
+
let windowStart = Date.now();
|
|
156
|
+
let lastBusyWarn = 0;
|
|
157
|
+
let totalReads = 0;
|
|
149
158
|
try {
|
|
150
159
|
while(true){
|
|
151
160
|
const { done, value } = await reader.read();
|
|
152
161
|
if (done) break;
|
|
162
|
+
totalReads++;
|
|
163
|
+
readCount++;
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const elapsed = now - windowStart;
|
|
166
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
167
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
168
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
169
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
170
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
171
|
+
lastBusyWarn = now;
|
|
172
|
+
}
|
|
173
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
174
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
175
|
+
readCount = 0;
|
|
176
|
+
windowStart = Date.now();
|
|
177
|
+
}
|
|
153
178
|
this.processFrame(value);
|
|
154
179
|
}
|
|
155
180
|
} catch (error) {
|
|
156
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
181
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
157
182
|
await this.disconnect();
|
|
158
183
|
}
|
|
184
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
159
185
|
}
|
|
160
186
|
processFrame(packet) {
|
|
161
187
|
if ('configuration' === packet.type) {
|
|
@@ -1170,14 +1196,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1170
1196
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1171
1197
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1172
1198
|
try {
|
|
1173
|
-
if (useShellScreencap
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1199
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1200
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1201
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1202
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1203
|
+
if (!screenshotBuffer) {
|
|
1204
|
+
this.takeScreenshotFailCount++;
|
|
1205
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1206
|
+
}
|
|
1207
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
1208
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1209
|
+
this.takeScreenshotFailCount++;
|
|
1210
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1211
|
+
}
|
|
1212
|
+
this.takeScreenshotFailCount = 0;
|
|
1213
|
+
} else {
|
|
1214
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1215
|
+
throw new Error('Using shell screencap directly');
|
|
1181
1216
|
}
|
|
1182
1217
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1183
1218
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1209,9 +1244,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1209
1244
|
if (!isValidImageBuffer(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1210
1245
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1211
1246
|
} finally{
|
|
1212
|
-
|
|
1213
|
-
|
|
1247
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1248
|
+
const child = execFile(adbPath, [
|
|
1249
|
+
'-s',
|
|
1250
|
+
this.deviceId,
|
|
1251
|
+
'shell',
|
|
1252
|
+
`rm ${androidScreenshotPath}`
|
|
1253
|
+
], {
|
|
1254
|
+
timeout: 3000
|
|
1255
|
+
}, (err)=>{
|
|
1256
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1214
1257
|
});
|
|
1258
|
+
child.unref();
|
|
1215
1259
|
}
|
|
1216
1260
|
}
|
|
1217
1261
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1655,6 +1699,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1655
1699
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1656
1700
|
device_define_property(this, "appNameMapping", {});
|
|
1657
1701
|
device_define_property(this, "cachedAdjustScale", null);
|
|
1702
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1658
1703
|
device_define_property(this, "interfaceType", 'android');
|
|
1659
1704
|
device_define_property(this, "uri", void 0);
|
|
1660
1705
|
device_define_property(this, "options", void 0);
|
|
@@ -1664,6 +1709,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1664
1709
|
this.customActions = options?.customActions;
|
|
1665
1710
|
}
|
|
1666
1711
|
}
|
|
1712
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1667
1713
|
const runAdbShellParamSchema = z.object({
|
|
1668
1714
|
command: z.string().describe('ADB shell command to execute')
|
|
1669
1715
|
});
|
package/dist/lib/cli.js
CHANGED
|
@@ -40,6 +40,10 @@ var __webpack_modules__ = {
|
|
|
40
40
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
41
41
|
const MAX_SCAN_BYTES = 1000;
|
|
42
42
|
const CONNECTION_WAIT_MS = 1000;
|
|
43
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
44
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
45
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
46
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
43
47
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
44
48
|
enabled: false,
|
|
45
49
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -137,16 +141,37 @@ var __webpack_modules__ = {
|
|
|
137
141
|
this.consumeFramesLoop(reader);
|
|
138
142
|
}
|
|
139
143
|
async consumeFramesLoop(reader) {
|
|
144
|
+
let readCount = 0;
|
|
145
|
+
let windowStart = Date.now();
|
|
146
|
+
let lastBusyWarn = 0;
|
|
147
|
+
let totalReads = 0;
|
|
140
148
|
try {
|
|
141
149
|
while(true){
|
|
142
150
|
const { done, value } = await reader.read();
|
|
143
151
|
if (done) break;
|
|
152
|
+
totalReads++;
|
|
153
|
+
readCount++;
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const elapsed = now - windowStart;
|
|
156
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
157
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
158
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
159
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
160
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
161
|
+
lastBusyWarn = now;
|
|
162
|
+
}
|
|
163
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
164
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
165
|
+
readCount = 0;
|
|
166
|
+
windowStart = Date.now();
|
|
167
|
+
}
|
|
144
168
|
this.processFrame(value);
|
|
145
169
|
}
|
|
146
170
|
} catch (error) {
|
|
147
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
171
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
148
172
|
await this.disconnect();
|
|
149
173
|
}
|
|
174
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
150
175
|
}
|
|
151
176
|
processFrame(packet) {
|
|
152
177
|
if ('configuration' === packet.type) {
|
|
@@ -525,6 +550,7 @@ var __webpack_exports__ = {};
|
|
|
525
550
|
};
|
|
526
551
|
const external_node_assert_namespaceObject = require("node:assert");
|
|
527
552
|
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
553
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
528
554
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
529
555
|
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
|
|
530
556
|
var external_node_module_ = __webpack_require__("node:module");
|
|
@@ -1186,14 +1212,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1186
1212
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1187
1213
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1188
1214
|
try {
|
|
1189
|
-
if (useShellScreencap
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1215
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1216
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1217
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1218
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1219
|
+
if (!screenshotBuffer) {
|
|
1220
|
+
this.takeScreenshotFailCount++;
|
|
1221
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1222
|
+
}
|
|
1223
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
|
|
1224
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1225
|
+
this.takeScreenshotFailCount++;
|
|
1226
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1227
|
+
}
|
|
1228
|
+
this.takeScreenshotFailCount = 0;
|
|
1229
|
+
} else {
|
|
1230
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1231
|
+
throw new Error('Using shell screencap directly');
|
|
1197
1232
|
}
|
|
1198
1233
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1199
1234
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1225,9 +1260,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1225
1260
|
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1226
1261
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1227
1262
|
} finally{
|
|
1228
|
-
|
|
1229
|
-
|
|
1263
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1264
|
+
const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
|
|
1265
|
+
'-s',
|
|
1266
|
+
this.deviceId,
|
|
1267
|
+
'shell',
|
|
1268
|
+
`rm ${androidScreenshotPath}`
|
|
1269
|
+
], {
|
|
1270
|
+
timeout: 3000
|
|
1271
|
+
}, (err)=>{
|
|
1272
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1230
1273
|
});
|
|
1274
|
+
child.unref();
|
|
1231
1275
|
}
|
|
1232
1276
|
}
|
|
1233
1277
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1671,6 +1715,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1671
1715
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1672
1716
|
device_define_property(this, "appNameMapping", {});
|
|
1673
1717
|
device_define_property(this, "cachedAdjustScale", null);
|
|
1718
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1674
1719
|
device_define_property(this, "interfaceType", 'android');
|
|
1675
1720
|
device_define_property(this, "uri", void 0);
|
|
1676
1721
|
device_define_property(this, "options", void 0);
|
|
@@ -1680,6 +1725,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1680
1725
|
this.customActions = options?.customActions;
|
|
1681
1726
|
}
|
|
1682
1727
|
}
|
|
1728
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1683
1729
|
const runAdbShellParamSchema = core_namespaceObject.z.object({
|
|
1684
1730
|
command: core_namespaceObject.z.string().describe('ADB shell command to execute')
|
|
1685
1731
|
});
|
package/dist/lib/index.js
CHANGED
|
@@ -40,6 +40,10 @@ var __webpack_modules__ = {
|
|
|
40
40
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
41
41
|
const MAX_SCAN_BYTES = 1000;
|
|
42
42
|
const CONNECTION_WAIT_MS = 1000;
|
|
43
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
44
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
45
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
46
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
43
47
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
44
48
|
enabled: false,
|
|
45
49
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -137,16 +141,37 @@ var __webpack_modules__ = {
|
|
|
137
141
|
this.consumeFramesLoop(reader);
|
|
138
142
|
}
|
|
139
143
|
async consumeFramesLoop(reader) {
|
|
144
|
+
let readCount = 0;
|
|
145
|
+
let windowStart = Date.now();
|
|
146
|
+
let lastBusyWarn = 0;
|
|
147
|
+
let totalReads = 0;
|
|
140
148
|
try {
|
|
141
149
|
while(true){
|
|
142
150
|
const { done, value } = await reader.read();
|
|
143
151
|
if (done) break;
|
|
152
|
+
totalReads++;
|
|
153
|
+
readCount++;
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const elapsed = now - windowStart;
|
|
156
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
157
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
158
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
159
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
160
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
161
|
+
lastBusyWarn = now;
|
|
162
|
+
}
|
|
163
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
164
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
165
|
+
readCount = 0;
|
|
166
|
+
windowStart = Date.now();
|
|
167
|
+
}
|
|
144
168
|
this.processFrame(value);
|
|
145
169
|
}
|
|
146
170
|
} catch (error) {
|
|
147
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
171
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
148
172
|
await this.disconnect();
|
|
149
173
|
}
|
|
174
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
150
175
|
}
|
|
151
176
|
processFrame(packet) {
|
|
152
177
|
if ('configuration' === packet.type) {
|
|
@@ -442,6 +467,7 @@ var __webpack_exports__ = {};
|
|
|
442
467
|
});
|
|
443
468
|
const external_node_assert_namespaceObject = require("node:assert");
|
|
444
469
|
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
470
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
445
471
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
446
472
|
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
|
|
447
473
|
var external_node_module_ = __webpack_require__("node:module");
|
|
@@ -1106,14 +1132,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1106
1132
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1107
1133
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1108
1134
|
try {
|
|
1109
|
-
if (useShellScreencap
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1135
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1136
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1137
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1138
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1139
|
+
if (!screenshotBuffer) {
|
|
1140
|
+
this.takeScreenshotFailCount++;
|
|
1141
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1142
|
+
}
|
|
1143
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
|
|
1144
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1145
|
+
this.takeScreenshotFailCount++;
|
|
1146
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1147
|
+
}
|
|
1148
|
+
this.takeScreenshotFailCount = 0;
|
|
1149
|
+
} else {
|
|
1150
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1151
|
+
throw new Error('Using shell screencap directly');
|
|
1117
1152
|
}
|
|
1118
1153
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1119
1154
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1145,9 +1180,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1145
1180
|
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1146
1181
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1147
1182
|
} finally{
|
|
1148
|
-
|
|
1149
|
-
|
|
1183
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1184
|
+
const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
|
|
1185
|
+
'-s',
|
|
1186
|
+
this.deviceId,
|
|
1187
|
+
'shell',
|
|
1188
|
+
`rm ${androidScreenshotPath}`
|
|
1189
|
+
], {
|
|
1190
|
+
timeout: 3000
|
|
1191
|
+
}, (err)=>{
|
|
1192
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1150
1193
|
});
|
|
1194
|
+
child.unref();
|
|
1151
1195
|
}
|
|
1152
1196
|
}
|
|
1153
1197
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1591,6 +1635,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1591
1635
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1592
1636
|
device_define_property(this, "appNameMapping", {});
|
|
1593
1637
|
device_define_property(this, "cachedAdjustScale", null);
|
|
1638
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1594
1639
|
device_define_property(this, "interfaceType", 'android');
|
|
1595
1640
|
device_define_property(this, "uri", void 0);
|
|
1596
1641
|
device_define_property(this, "options", void 0);
|
|
@@ -1600,6 +1645,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1600
1645
|
this.customActions = options?.customActions;
|
|
1601
1646
|
}
|
|
1602
1647
|
}
|
|
1648
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1603
1649
|
const runAdbShellParamSchema = core_namespaceObject.z.object({
|
|
1604
1650
|
command: core_namespaceObject.z.string().describe('ADB shell command to execute')
|
|
1605
1651
|
});
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -40,6 +40,10 @@ var __webpack_modules__ = {
|
|
|
40
40
|
const KEYFRAME_POLL_INTERVAL_MS = 200;
|
|
41
41
|
const MAX_SCAN_BYTES = 1000;
|
|
42
42
|
const CONNECTION_WAIT_MS = 1000;
|
|
43
|
+
const BUSY_LOOP_WINDOW_MS = 1000;
|
|
44
|
+
const BUSY_LOOP_MAX_READS = 500;
|
|
45
|
+
const BUSY_LOOP_COOLDOWN_MS = 50;
|
|
46
|
+
const BUSY_LOOP_WARN_INTERVAL_MS = 5000;
|
|
43
47
|
const DEFAULT_SCRCPY_CONFIG = {
|
|
44
48
|
enabled: false,
|
|
45
49
|
maxSize: DEFAULT_MAX_SIZE,
|
|
@@ -137,16 +141,37 @@ var __webpack_modules__ = {
|
|
|
137
141
|
this.consumeFramesLoop(reader);
|
|
138
142
|
}
|
|
139
143
|
async consumeFramesLoop(reader) {
|
|
144
|
+
let readCount = 0;
|
|
145
|
+
let windowStart = Date.now();
|
|
146
|
+
let lastBusyWarn = 0;
|
|
147
|
+
let totalReads = 0;
|
|
140
148
|
try {
|
|
141
149
|
while(true){
|
|
142
150
|
const { done, value } = await reader.read();
|
|
143
151
|
if (done) break;
|
|
152
|
+
totalReads++;
|
|
153
|
+
readCount++;
|
|
154
|
+
const now = Date.now();
|
|
155
|
+
const elapsed = now - windowStart;
|
|
156
|
+
if (elapsed >= BUSY_LOOP_WINDOW_MS) {
|
|
157
|
+
const readsPerSec = readCount / elapsed * 1000;
|
|
158
|
+
if (readCount > BUSY_LOOP_MAX_READS) {
|
|
159
|
+
if (now - lastBusyWarn >= BUSY_LOOP_WARN_INTERVAL_MS) {
|
|
160
|
+
warnScrcpy(`[CPU-DIAG] Possible busy loop detected! ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(0)} reads/sec). Total reads: ${totalReads}. Throttling with ${BUSY_LOOP_COOLDOWN_MS}ms delay.`);
|
|
161
|
+
lastBusyWarn = now;
|
|
162
|
+
}
|
|
163
|
+
await new Promise((resolve)=>setTimeout(resolve, BUSY_LOOP_COOLDOWN_MS));
|
|
164
|
+
} else debugScrcpy(`[CPU-DIAG] Frame loop stats: ${readCount} reads in ${elapsed}ms (${readsPerSec.toFixed(1)} reads/sec), total: ${totalReads}`);
|
|
165
|
+
readCount = 0;
|
|
166
|
+
windowStart = Date.now();
|
|
167
|
+
}
|
|
144
168
|
this.processFrame(value);
|
|
145
169
|
}
|
|
146
170
|
} catch (error) {
|
|
147
|
-
debugScrcpy(`Frame consumer error: ${error}`);
|
|
171
|
+
debugScrcpy(`Frame consumer error (total reads: ${totalReads}): ${error}`);
|
|
148
172
|
await this.disconnect();
|
|
149
173
|
}
|
|
174
|
+
debugScrcpy(`Frame consumer loop ended normally (total reads: ${totalReads})`);
|
|
150
175
|
}
|
|
151
176
|
processFrame(packet) {
|
|
152
177
|
if ('configuration' === packet.type) {
|
|
@@ -539,6 +564,7 @@ var __webpack_exports__ = {};
|
|
|
539
564
|
};
|
|
540
565
|
const external_node_assert_namespaceObject = require("node:assert");
|
|
541
566
|
var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
|
|
567
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
542
568
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
543
569
|
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_);
|
|
544
570
|
var external_node_module_ = __webpack_require__("node:module");
|
|
@@ -1201,14 +1227,23 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1201
1227
|
const androidScreenshotPath = `/data/local/tmp/ms_${screenshotId}.png`;
|
|
1202
1228
|
const useShellScreencap = 'number' == typeof this.options?.displayId;
|
|
1203
1229
|
try {
|
|
1204
|
-
if (useShellScreencap
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1230
|
+
if (!useShellScreencap && this.takeScreenshotFailCount < AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) {
|
|
1231
|
+
debugDevice('Taking screenshot via adb.takeScreenshot');
|
|
1232
|
+
screenshotBuffer = await adb.takeScreenshot(null);
|
|
1233
|
+
debugDevice('adb.takeScreenshot completed');
|
|
1234
|
+
if (!screenshotBuffer) {
|
|
1235
|
+
this.takeScreenshotFailCount++;
|
|
1236
|
+
throw new Error('Failed to capture screenshot: screenshotBuffer is null');
|
|
1237
|
+
}
|
|
1238
|
+
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) {
|
|
1239
|
+
debugDevice('Invalid image buffer detected: not a valid image format');
|
|
1240
|
+
this.takeScreenshotFailCount++;
|
|
1241
|
+
throw new Error('Screenshot buffer has invalid format: could not find valid image signature');
|
|
1242
|
+
}
|
|
1243
|
+
this.takeScreenshotFailCount = 0;
|
|
1244
|
+
} else {
|
|
1245
|
+
if (this.takeScreenshotFailCount >= AndroidDevice.TAKE_SCREENSHOT_FAIL_THRESHOLD) debugDevice('Skipping takeScreenshot (failed %d consecutive times), using shell screencap directly', this.takeScreenshotFailCount);
|
|
1246
|
+
throw new Error('Using shell screencap directly');
|
|
1212
1247
|
}
|
|
1213
1248
|
const validScreenshotBufferSize = this.options?.minScreenshotBufferSize ?? 10240;
|
|
1214
1249
|
if (validScreenshotBufferSize > 0 && screenshotBuffer.length < validScreenshotBufferSize) {
|
|
@@ -1240,9 +1275,18 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1240
1275
|
if (!(0, img_namespaceObject.isValidImageBuffer)(screenshotBuffer)) throw new Error('Fallback screenshot buffer has invalid PNG format');
|
|
1241
1276
|
debugDevice(`Fallback screenshot validated successfully: ${screenshotBuffer.length} bytes`);
|
|
1242
1277
|
} finally{
|
|
1243
|
-
|
|
1244
|
-
|
|
1278
|
+
const adbPath = adb.executable?.path ?? 'adb';
|
|
1279
|
+
const child = (0, external_node_child_process_namespaceObject.execFile)(adbPath, [
|
|
1280
|
+
'-s',
|
|
1281
|
+
this.deviceId,
|
|
1282
|
+
'shell',
|
|
1283
|
+
`rm ${androidScreenshotPath}`
|
|
1284
|
+
], {
|
|
1285
|
+
timeout: 3000
|
|
1286
|
+
}, (err)=>{
|
|
1287
|
+
if (err) debugDevice('Failed to delete remote screenshot: %s', err.message);
|
|
1245
1288
|
});
|
|
1289
|
+
child.unref();
|
|
1246
1290
|
}
|
|
1247
1291
|
}
|
|
1248
1292
|
if (!screenshotBuffer) throw new Error('Failed to capture screenshot: all methods failed');
|
|
@@ -1686,6 +1730,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1686
1730
|
device_define_property(this, "scrcpyAdapter", null);
|
|
1687
1731
|
device_define_property(this, "appNameMapping", {});
|
|
1688
1732
|
device_define_property(this, "cachedAdjustScale", null);
|
|
1733
|
+
device_define_property(this, "takeScreenshotFailCount", 0);
|
|
1689
1734
|
device_define_property(this, "interfaceType", 'android');
|
|
1690
1735
|
device_define_property(this, "uri", void 0);
|
|
1691
1736
|
device_define_property(this, "options", void 0);
|
|
@@ -1695,6 +1740,7 @@ ${Object.keys(size).filter((key)=>size[key]).map((key)=>` ${key} size: ${size[k
|
|
|
1695
1740
|
this.customActions = options?.customActions;
|
|
1696
1741
|
}
|
|
1697
1742
|
}
|
|
1743
|
+
device_define_property(AndroidDevice, "TAKE_SCREENSHOT_FAIL_THRESHOLD", 3);
|
|
1698
1744
|
const runAdbShellParamSchema = core_namespaceObject.z.object({
|
|
1699
1745
|
command: core_namespaceObject.z.string().describe('ADB shell command to execute')
|
|
1700
1746
|
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare class AndroidDevice implements AbstractInterface {
|
|
|
74
74
|
private scrcpyAdapter;
|
|
75
75
|
private appNameMapping;
|
|
76
76
|
private cachedAdjustScale;
|
|
77
|
+
private takeScreenshotFailCount;
|
|
78
|
+
private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
|
|
77
79
|
interfaceType: InterfaceType;
|
|
78
80
|
uri: string | undefined;
|
|
79
81
|
options?: AndroidDeviceOpt;
|
|
@@ -337,6 +339,8 @@ declare class ScrcpyScreenshotManager {
|
|
|
337
339
|
private startFrameConsumer;
|
|
338
340
|
/**
|
|
339
341
|
* Main frame consumption loop
|
|
342
|
+
* Includes busy-loop detection: if reader.read() resolves too fast
|
|
343
|
+
* (e.g. broken stream returning immediately), we throttle to prevent 100% CPU.
|
|
340
344
|
*/
|
|
341
345
|
private consumeFramesLoop;
|
|
342
346
|
/**
|
|
@@ -75,6 +75,8 @@ declare class AndroidDevice implements AbstractInterface {
|
|
|
75
75
|
private scrcpyAdapter;
|
|
76
76
|
private appNameMapping;
|
|
77
77
|
private cachedAdjustScale;
|
|
78
|
+
private takeScreenshotFailCount;
|
|
79
|
+
private static readonly TAKE_SCREENSHOT_FAIL_THRESHOLD;
|
|
78
80
|
interfaceType: InterfaceType;
|
|
79
81
|
uri: string | undefined;
|
|
80
82
|
options?: AndroidDeviceOpt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@midscene/android",
|
|
3
|
-
"version": "1.5.3
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Android automation library for Midscene",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Android UI automation",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@yume-chan/stream-extra": "^1.0.0",
|
|
42
42
|
"appium-adb": "12.12.1",
|
|
43
43
|
"sharp": "^0.34.3",
|
|
44
|
-
"@midscene/core": "1.5.3
|
|
45
|
-
"@midscene/shared": "1.5.3
|
|
44
|
+
"@midscene/core": "1.5.3",
|
|
45
|
+
"@midscene/shared": "1.5.3"
|
|
46
46
|
},
|
|
47
47
|
"optionalDependencies": {
|
|
48
48
|
"@ffmpeg-installer/ffmpeg": "^1.1.0"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"tsx": "^4.19.2",
|
|
57
57
|
"vitest": "3.0.5",
|
|
58
58
|
"zod": "3.24.3",
|
|
59
|
-
"@midscene/playground": "1.5.3
|
|
59
|
+
"@midscene/playground": "1.5.3"
|
|
60
60
|
},
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"scripts": {
|