@windypro-rourou/dsh-logcat 0.2.9 → 0.3.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 +14 -12
- package/lib/client.js +5 -1
- package/lib/index.js +622 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,23 +14,25 @@ DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视
|
|
|
14
14
|
- 级别过滤(V/D/I/W/E/F 单选,颜色与 Android Studio 一致)
|
|
15
15
|
- 关键词过滤、**测试包名输入框**(回车设置,与 agent 的 `logcat_set_package` 互通,状态栏实时显示)
|
|
16
16
|
- **截图按钮**:一键截取真机屏幕并下载 PNG(`exec-out screencap`)
|
|
17
|
+
- **崩溃高亮**:FATAL EXCEPTION / ANR 行红色高亮,一眼定位崩溃
|
|
17
18
|
- 暂停/继续(暂停时缓冲,恢复自动回放)、清空、复制、导出 .txt
|
|
18
19
|
- 窗口化渲染 + 自动滚动(滚动手动上翻时自动停用)
|
|
19
20
|
- 未授权设备提示「请在手机上点击允许 USB 调试」
|
|
20
|
-
- **Agent
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
设置了测试包名(或显式传 `package`)时自动按该 app 的 pid 过滤日志。
|
|
21
|
+
- **Agent 工具**(共 18 个,全部对 agent 开放,前置提示中已明示可调用):
|
|
22
|
+
- 设备:`logcat_devices`(列出设备)、`device_info`(型号/版本/SDK/分辨率/内存/电量)、`device_stats`(CPU/内存/电量实时采样)
|
|
23
|
+
- 执行:`adb_exec`(shell)、`adb_install`(本地 APK 装真机)、`adb_pull`(拉文件)
|
|
24
|
+
- 输入:`input_tap` / `input_swipe` / `input_text`(真机 UI 自动化)、`ui_dump`(界面层级 XML)
|
|
25
|
+
- 日志:`logcat_recent`(按包名/级别/关键词过滤)、`logcat_crash`(崩溃/ANR 自动捕获 + 上下文)、
|
|
26
|
+
`logcat_set_package`(锁定当前测试包名)
|
|
27
|
+
- 逆向/内存:`proc_list`(进程列表)、`proc_maps`(内存映射 + so 模块基址)、`proc_status`(进程状态/内存摘要)、
|
|
28
|
+
`mem_dump`(指定地址读内存 hex)、`frida_server`(frida-server 部署/启停)
|
|
29
29
|
- **实机调试工作流**:构建安卓应用时,agent 的通告会动态列出当前已连接设备(serial + 型号)与当前测试包名,
|
|
30
|
-
|
|
31
|
-
`logcat_recent`
|
|
30
|
+
可先向用户确认后:`adb_install` 部署 APK → `adb_exec` 启动 → `logcat_set_package` 锁包 →
|
|
31
|
+
`logcat_recent` / `logcat_crash` 看崩溃 → `ui_dump` + `input_*` 做界面自动化 → `adb_pull` 取证,闭环真机调试。
|
|
32
|
+
- **逆向工作流**:`proc_list` 定位进程 → `proc_maps` 拿模块基址/权限 → `mem_dump` 读目标地址 →
|
|
33
|
+
`frida_server` 起 frida 做动态插桩。读其他应用内存/maps 需要 root 或 debuggable 应用(run-as),工具会给出明确提示。
|
|
32
34
|
- **附加能力**:`POST /api/dsh-logcat/exec` 执行 shell、`POST /api/dsh-logcat/package` 设置包名、
|
|
33
|
-
`GET /api/dsh-logcat/screenshot`
|
|
35
|
+
`GET /api/dsh-logcat/screenshot` 截屏、`POST /api/dsh-logcat/install-adb` 一键装 adb。
|
|
34
36
|
|
|
35
37
|
## 安装
|
|
36
38
|
|
package/lib/client.js
CHANGED
|
@@ -71,6 +71,9 @@ window.__ModuleLoader__.load({
|
|
|
71
71
|
.lc-line .lv.W { color: #fbc02d; } .lc-line .lv.E { color: #ef5350; } .lc-line .lv.F { color: #ab47bc; }
|
|
72
72
|
.lc-line .tag { color: #29b6f6; margin-right: 8px; }
|
|
73
73
|
.lc-line.cont .msg { padding-left: 44px; color: var(--lc-dim, #9e9e9e); }
|
|
74
|
+
.lc-line.crash { background: rgba(239,83,80,.14); box-shadow: inset 2px 0 0 #ef5350; }
|
|
75
|
+
.lc-line.crash .tag { color: #ef5350; font-weight: 700; }
|
|
76
|
+
.lc-line.crash .msg { color: #ef5350; }
|
|
74
77
|
.lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--lc-dim, #9e9e9e); font-size: 13px; }
|
|
75
78
|
.lc-status { display: flex; align-items: center; gap: 14px; padding: 4px 12px; border-top: 1px solid rgba(128,128,128,.25); flex: none; font-size: 11px; color: var(--lc-dim, #9e9e9e); }
|
|
76
79
|
.lc-status b { font-weight: 600; color: inherit; }
|
|
@@ -487,13 +490,14 @@ window.__ModuleLoader__.load({
|
|
|
487
490
|
}, [scrollTop, entries.length]);
|
|
488
491
|
|
|
489
492
|
const rows = [];
|
|
493
|
+
const isCrash = (e) => /FATAL EXCEPTION|ANR in /.test(e.raw) || (e.tag === "AndroidRuntime" && e.level === "F");
|
|
490
494
|
for (let i = viewport.top; i < viewport.bottom && i < entries.length; i++) {
|
|
491
495
|
const e = entries[i];
|
|
492
496
|
const level = e.level !== "" ? e.level : " ";
|
|
493
497
|
rows.push(
|
|
494
498
|
h("div", {
|
|
495
499
|
key: i,
|
|
496
|
-
className: "lc-line" + (e.cont === true ? " cont" : ""),
|
|
500
|
+
className: "lc-line" + (e.cont === true ? " cont" : "") + (isCrash(e) ? " crash" : ""),
|
|
497
501
|
style: { top: i * ROW },
|
|
498
502
|
title: e.raw,
|
|
499
503
|
},
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,17 @@ const SECTION_ORDER = 152
|
|
|
36
36
|
|
|
37
37
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
38
38
|
export const LOGCAT_GUIDANCE =
|
|
39
|
-
'本机已安装 dsh-logcat 插件(DSH Web GUI
|
|
39
|
+
'本机已安装 dsh-logcat 插件(DSH Web GUI 的安卓实机调试/逆向工作台):自动探测本机 adb 并对已连接的调试设备附加 logcat 流;侧边栏「Logcat」入口打开面板(设备切换、级别/关键词过滤、按测试包名过滤、截图、崩溃高亮、一键安装 adb)。' +
|
|
40
|
+
'以下 agent 工具均可用(串口为设备 serial,可用 logcat_devices 获取),构建安卓应用/实机调试/逆向分析时请主动调用:' +
|
|
41
|
+
'设备与状态:logcat_devices(列出设备)、device_info(型号/版本/SDK/分辨率/内存/电量)、device_stats(CPU/内存/电量采样);' +
|
|
42
|
+
'部署与执行:adb_install(本地 APK 装真机)、adb_exec(任意 adb shell 命令)、adb_pull(拉文件)、input_tap/input_swipe/input_text(模拟点击/滑动/输入)、ui_dump(界面层级 XML);' +
|
|
43
|
+
'日志与崩溃:logcat_recent(按级别/关键词/包名读日志)、logcat_crash(崩溃/ANR 捕获+上下文)、logcat_set_package(锁定当前测试包名);' +
|
|
44
|
+
'逆向与内存:proc_list(进程列表)、proc_maps(内存映射+so 模块基址)、proc_status(进程状态/内存摘要)、mem_dump(指定地址读内存 hex)、frida_server(frida-server 部署/启停)。' +
|
|
45
|
+
'工作流建议:构建安卓应用 → 确认设备在线后 adb_install 部署、adb_exec 启动、logcat_set_package 锁包、logcat_recent/logcat_crash 看崩溃、ui_dump+input_* 做界面自动化;' +
|
|
46
|
+
'逆向分析 → proc_list 找进程、proc_maps 拿模块基址、mem_dump 读内存、frida_server 起 frida。' +
|
|
47
|
+
'限制:需设备开启 USB 调试并授权本机;读其他应用内存/maps 需要 root 或 debuggable 应用(run-as);logcat 输出可能含敏感信息;' +
|
|
48
|
+
'adb 命令消耗真实设备资源,破坏性操作(卸载/重启/清数据/杀进程)先向用户确认再执行。' +
|
|
49
|
+
'用户提到「Logcat / 安卓日志 / 实机调试 / adb 日志 / 逆向 / 读内存 / 抓包定位」时即指本插件,请据此主动协作。'
|
|
40
50
|
|
|
41
51
|
/** Dynamic model-facing announcement: base guidance plus currently attached devices. */
|
|
42
52
|
function logcatGuidance(engine) {
|
|
@@ -911,6 +921,605 @@ function adbPullTool(engine) {
|
|
|
911
921
|
})
|
|
912
922
|
}
|
|
913
923
|
|
|
924
|
+
/** The logcat_crash agent tool: find recent crash/ANR blocks with context. */
|
|
925
|
+
function logcatCrashTool(engine) {
|
|
926
|
+
return defineTool({
|
|
927
|
+
name: 'logcat_crash',
|
|
928
|
+
description: 'Find recent Android crash/ANR blocks (FATAL EXCEPTION / ANR in / AndroidRuntime) in the buffered logcat, with surrounding context lines. ' +
|
|
929
|
+
'Triggers: check whether the app crashed, get the exception stack for a crash.',
|
|
930
|
+
parameters: {
|
|
931
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
932
|
+
lines: { type: 'integer', description: 'Scan window into the ring buffer (default 2000, max 2000).' },
|
|
933
|
+
context: { type: 'integer', description: 'Lines of context around each crash marker (default 8).' },
|
|
934
|
+
},
|
|
935
|
+
output: {
|
|
936
|
+
schema: {
|
|
937
|
+
type: 'object',
|
|
938
|
+
additionalProperties: false,
|
|
939
|
+
properties: {
|
|
940
|
+
crashes: {
|
|
941
|
+
type: 'array',
|
|
942
|
+
required: true,
|
|
943
|
+
items: {
|
|
944
|
+
type: 'object',
|
|
945
|
+
additionalProperties: false,
|
|
946
|
+
properties: {
|
|
947
|
+
ts: { type: 'string', required: true },
|
|
948
|
+
pid: { type: 'integer', required: true },
|
|
949
|
+
tag: { type: 'string', required: true },
|
|
950
|
+
kind: { type: 'string', required: true },
|
|
951
|
+
summary: { type: 'string', required: true },
|
|
952
|
+
lines: { type: 'array', required: true, items: { type: 'string' } },
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
render: (_args, value) => {
|
|
959
|
+
const crashes = value?.crashes ?? []
|
|
960
|
+
if (crashes.length === 0) return [{ type: 'text', text: '(no crash / ANR found in the scanned window)' }]
|
|
961
|
+
const blocks = crashes.map((c) => `${c.ts} [${c.kind}] pid=${c.pid} ${c.tag}\n ${c.summary}\n ` + c.lines.join('\n '))
|
|
962
|
+
return [{ type: 'text', text: blocks.join('\n\n') }]
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
async execute(args) {
|
|
966
|
+
const devices = engine.deviceList()
|
|
967
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
968
|
+
if (serial === '') return { crashes: [] }
|
|
969
|
+
const scan = Math.min(Math.max(Number(args.lines ?? 2000) || 2000, 1), 2000)
|
|
970
|
+
const context = Math.min(Math.max(Number(args.context ?? 8) || 8, 0), 40)
|
|
971
|
+
const entries = engine.recent(serial, scan)
|
|
972
|
+
const MARKER = /FATAL EXCEPTION|ANR in |AndroidRuntime:/
|
|
973
|
+
const crashes = []
|
|
974
|
+
for (let i = 0; i < entries.length; i++) {
|
|
975
|
+
const e = entries[i]
|
|
976
|
+
if (!MARKER.test(e.raw)) continue
|
|
977
|
+
const start = Math.max(0, i - context)
|
|
978
|
+
const end = Math.min(entries.length, i + context + 1)
|
|
979
|
+
const block = entries.slice(start, end)
|
|
980
|
+
const first = block.find((b) => /FATAL EXCEPTION|ANR in /.test(b.raw)) ?? e
|
|
981
|
+
crashes.push({
|
|
982
|
+
ts: first.ts,
|
|
983
|
+
pid: first.pid,
|
|
984
|
+
tag: first.tag,
|
|
985
|
+
kind: /ANR in /.test(first.raw) ? 'ANR' : 'FATAL',
|
|
986
|
+
summary: first.msg.slice(0, 240),
|
|
987
|
+
lines: block.map((b) => b.raw),
|
|
988
|
+
})
|
|
989
|
+
i = end - 1
|
|
990
|
+
}
|
|
991
|
+
return { crashes }
|
|
992
|
+
},
|
|
993
|
+
})
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/** The device_info agent tool: structured device facts. */
|
|
997
|
+
function deviceInfoTool(engine) {
|
|
998
|
+
return defineTool({
|
|
999
|
+
name: 'device_info',
|
|
1000
|
+
description: 'Get structured info about an attached Android device (model, brand, Android version, SDK, resolution, ABI, total memory, battery) — ' +
|
|
1001
|
+
'for environment/version checks before debugging.',
|
|
1002
|
+
parameters: {
|
|
1003
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1004
|
+
},
|
|
1005
|
+
output: {
|
|
1006
|
+
schema: {
|
|
1007
|
+
type: 'object',
|
|
1008
|
+
additionalProperties: false,
|
|
1009
|
+
properties: {
|
|
1010
|
+
serial: { type: 'string', required: true },
|
|
1011
|
+
model: { type: 'string' },
|
|
1012
|
+
brand: { type: 'string' },
|
|
1013
|
+
androidVersion: { type: 'string' },
|
|
1014
|
+
sdk: { type: 'string' },
|
|
1015
|
+
resolution: { type: 'string' },
|
|
1016
|
+
abi: { type: 'string' },
|
|
1017
|
+
totalMemKb: { type: 'integer' },
|
|
1018
|
+
batteryLevel: { type: 'integer' },
|
|
1019
|
+
batteryStatus: { type: 'string' },
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
render: (_args, value) => {
|
|
1023
|
+
const rows = ['model', 'brand', 'androidVersion', 'sdk', 'resolution', 'abi', 'totalMemKb', 'batteryLevel', 'batteryStatus']
|
|
1024
|
+
.filter((k) => value?.[k] !== undefined && value[k] !== '')
|
|
1025
|
+
.map((k) => `${k}: ${value[k]}`)
|
|
1026
|
+
return [{ type: 'text', text: rows.length > 0 ? rows.join('\n') : '(no device)' }]
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
async execute(args) {
|
|
1030
|
+
const devices = engine.deviceList()
|
|
1031
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
1032
|
+
if (serial === '' || engine.adb === null) return { serial: '' }
|
|
1033
|
+
const sh = 'getprop ro.product.model; getprop ro.product.brand; getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.product.cpu.abi; wm size; grep MemTotal /proc/meminfo; dumpsys battery | grep -E "level:|status:"'
|
|
1034
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
|
|
1035
|
+
const lines = (result.stdout ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
1036
|
+
const get = (i) => lines[i] ?? ''
|
|
1037
|
+
const memMatch = /MemTotal:\s*(\d+) kB/.exec(lines.find((l) => l.startsWith('MemTotal')) ?? '')
|
|
1038
|
+
const levelMatch = /level:\s*(\d+)/.exec(lines.find((l) => l.startsWith('level')) ?? '')
|
|
1039
|
+
const statusMatch = /status:\s*(\S+)/.exec(lines.find((l) => l.startsWith('status')) ?? '')
|
|
1040
|
+
return {
|
|
1041
|
+
serial,
|
|
1042
|
+
model: get(0) || undefined,
|
|
1043
|
+
brand: get(1) || undefined,
|
|
1044
|
+
androidVersion: get(2) || undefined,
|
|
1045
|
+
sdk: get(3) || undefined,
|
|
1046
|
+
abi: get(4) || undefined,
|
|
1047
|
+
resolution: (lines.find((l) => l.startsWith('Physical size')) ?? '').replace('Physical size: ', '') || undefined,
|
|
1048
|
+
totalMemKb: memMatch ? Number.parseInt(memMatch[1], 10) : undefined,
|
|
1049
|
+
batteryLevel: levelMatch ? Number.parseInt(levelMatch[1], 10) : undefined,
|
|
1050
|
+
batteryStatus: statusMatch?.[1] || undefined,
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
})
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** The device_stats agent tool: one-shot CPU / memory / battery sample. */
|
|
1057
|
+
function deviceStatsTool(engine) {
|
|
1058
|
+
return defineTool({
|
|
1059
|
+
name: 'device_stats',
|
|
1060
|
+
description: 'Take a one-shot sample of device CPU / memory / battery state — for performance and resource checks while debugging an app.',
|
|
1061
|
+
parameters: {
|
|
1062
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1063
|
+
},
|
|
1064
|
+
output: {
|
|
1065
|
+
schema: {
|
|
1066
|
+
type: 'object',
|
|
1067
|
+
additionalProperties: false,
|
|
1068
|
+
properties: {
|
|
1069
|
+
cpu: { type: 'string' },
|
|
1070
|
+
memTotalKb: { type: 'integer' },
|
|
1071
|
+
memFreeKb: { type: 'integer' },
|
|
1072
|
+
batteryLevel: { type: 'integer' },
|
|
1073
|
+
batteryStatus: { type: 'string' },
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
render: (_args, value) => {
|
|
1077
|
+
const rows = ['cpu', 'memTotalKb', 'memFreeKb', 'batteryLevel', 'batteryStatus']
|
|
1078
|
+
.filter((k) => value?.[k] !== undefined && value[k] !== '')
|
|
1079
|
+
.map((k) => `${k}: ${value[k]}`)
|
|
1080
|
+
return [{ type: 'text', text: rows.length > 0 ? rows.join('\n') : '(no device)' }]
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
async execute(args) {
|
|
1084
|
+
const devices = engine.deviceList()
|
|
1085
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
1086
|
+
if (serial === '' || engine.adb === null) return {}
|
|
1087
|
+
const sh = 'top -b -n 1 | head -4; grep -E "MemTotal|MemFree" /proc/meminfo; dumpsys battery | grep -E "level:|status:"'
|
|
1088
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
|
|
1089
|
+
const lines = (result.stdout ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
1090
|
+
const cpu = lines.find((l) => /%cpu|Cpu\(s\)|CPU:/i.test(l)) ?? undefined
|
|
1091
|
+
const totalMatch = /MemTotal:\s*(\d+) kB/.exec(lines.find((l) => l.startsWith('MemTotal')) ?? '')
|
|
1092
|
+
const freeMatch = /MemFree:\s*(\d+) kB/.exec(lines.find((l) => l.startsWith('MemFree')) ?? '')
|
|
1093
|
+
const levelMatch = /level:\s*(\d+)/.exec(lines.find((l) => l.startsWith('level')) ?? '')
|
|
1094
|
+
const statusMatch = /status:\s*(\S+)/.exec(lines.find((l) => l.startsWith('status')) ?? '')
|
|
1095
|
+
return {
|
|
1096
|
+
cpu,
|
|
1097
|
+
memTotalKb: totalMatch ? Number.parseInt(totalMatch[1], 10) : undefined,
|
|
1098
|
+
memFreeKb: freeMatch ? Number.parseInt(freeMatch[1], 10) : undefined,
|
|
1099
|
+
batteryLevel: levelMatch ? Number.parseInt(levelMatch[1], 10) : undefined,
|
|
1100
|
+
batteryStatus: statusMatch?.[1] || undefined,
|
|
1101
|
+
}
|
|
1102
|
+
},
|
|
1103
|
+
})
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** Shared runner for `adb shell input ...` commands. */
|
|
1107
|
+
function inputShellTool(engine, name, description, argSpec, buildCommand) {
|
|
1108
|
+
return defineTool({
|
|
1109
|
+
name,
|
|
1110
|
+
description,
|
|
1111
|
+
parameters: {
|
|
1112
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1113
|
+
...argSpec,
|
|
1114
|
+
},
|
|
1115
|
+
output: {
|
|
1116
|
+
schema: {
|
|
1117
|
+
type: 'object',
|
|
1118
|
+
additionalProperties: false,
|
|
1119
|
+
properties: {
|
|
1120
|
+
ok: { type: 'boolean', required: true },
|
|
1121
|
+
code: { type: 'integer' },
|
|
1122
|
+
stdout: { type: 'string', required: true },
|
|
1123
|
+
stderr: { type: 'string', required: true },
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
render: (_args, value) => {
|
|
1127
|
+
const out = value?.stdout ?? ''
|
|
1128
|
+
const err = value?.stderr ?? ''
|
|
1129
|
+
const parts = []
|
|
1130
|
+
if (out !== '') parts.push(out.trimEnd())
|
|
1131
|
+
if (err !== '') parts.push('stderr: ' + err.trimEnd())
|
|
1132
|
+
if (parts.length === 0) parts.push(value?.ok ? 'done' : `input failed (exit ${value?.code ?? '?'})`)
|
|
1133
|
+
return [{ type: 'text', text: parts.join('\n') }]
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
async execute(args) {
|
|
1137
|
+
const devices = engine.deviceList()
|
|
1138
|
+
const serial = typeof args.serial === 'string' && args.serial !== ''
|
|
1139
|
+
? args.serial
|
|
1140
|
+
: devices.find((d) => d.state === 'device')?.serial ?? ''
|
|
1141
|
+
if (engine.adb === null) return { ok: false, code: null, stdout: '', stderr: 'adb not found on this host' }
|
|
1142
|
+
if (serial === '') return { ok: false, code: null, stdout: '', stderr: 'no attached device; connect and authorize one first' }
|
|
1143
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', buildCommand(args)], 15000)
|
|
1144
|
+
return { ok: result.ok, code: result.code, stdout: result.stdout, stderr: result.stderr }
|
|
1145
|
+
},
|
|
1146
|
+
})
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** The input_tap agent tool: simulate a tap. */
|
|
1150
|
+
function inputTapTool(engine) {
|
|
1151
|
+
return inputShellTool(engine, 'input_tap',
|
|
1152
|
+
'Simulate a tap on the device screen at (x, y) — for UI automation flows. Combine with ui_dump to find element coordinates.',
|
|
1153
|
+
{ x: { type: 'integer', description: 'X coordinate (pixels).' }, y: { type: 'integer', description: 'Y coordinate (pixels).' } },
|
|
1154
|
+
(a) => `input tap ${Number(a.x) | 0} ${Number(a.y) | 0}`)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** The input_swipe agent tool: simulate a swipe. */
|
|
1158
|
+
function inputSwipeTool(engine) {
|
|
1159
|
+
return inputShellTool(engine, 'input_swipe',
|
|
1160
|
+
'Simulate a swipe/drag on the device screen from (x1,y1) to (x2,y2), optionally with a duration in ms.',
|
|
1161
|
+
{
|
|
1162
|
+
x1: { type: 'integer', description: 'Start X.' }, y1: { type: 'integer', description: 'Start Y.' },
|
|
1163
|
+
x2: { type: 'integer', description: 'End X.' }, y2: { type: 'integer', description: 'End Y.' },
|
|
1164
|
+
duration: { type: 'integer', description: 'Duration in ms (optional, default 200).' },
|
|
1165
|
+
},
|
|
1166
|
+
(a) => `input swipe ${Number(a.x1) | 0} ${Number(a.y1) | 0} ${Number(a.x2) | 0} ${Number(a.y2) | 0} ${Math.max(Number(a.duration ?? 200) || 200, 0)}`)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** The input_text agent tool: type text (spaces become %s, shell chars escaped). */
|
|
1170
|
+
function inputTextTool(engine) {
|
|
1171
|
+
return inputShellTool(engine, 'input_text',
|
|
1172
|
+
'Type text into the focused field on the device (adb shell input text). Spaces are sent as %s; escape special characters as needed.',
|
|
1173
|
+
{ text: { type: 'string', description: 'The text to type, e.g. "hello world".' } },
|
|
1174
|
+
(a) => {
|
|
1175
|
+
const raw = typeof a.text === 'string' ? a.text : ''
|
|
1176
|
+
const safe = raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/ /g, '%s')
|
|
1177
|
+
return `input text "${safe}"`
|
|
1178
|
+
})
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** The ui_dump agent tool: dump the current UI hierarchy (uiautomator). */
|
|
1182
|
+
function uiDumpTool(engine) {
|
|
1183
|
+
return defineTool({
|
|
1184
|
+
name: 'ui_dump',
|
|
1185
|
+
description: 'Dump the current UI hierarchy of the device screen as XML (uiautomator dump) — lets you see buttons/text/coordinates on screen for UI automation.',
|
|
1186
|
+
parameters: {
|
|
1187
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1188
|
+
},
|
|
1189
|
+
output: {
|
|
1190
|
+
schema: {
|
|
1191
|
+
type: 'object',
|
|
1192
|
+
additionalProperties: false,
|
|
1193
|
+
properties: {
|
|
1194
|
+
ok: { type: 'boolean', required: true },
|
|
1195
|
+
xml: { type: 'string' },
|
|
1196
|
+
error: { type: 'string' },
|
|
1197
|
+
},
|
|
1198
|
+
},
|
|
1199
|
+
render: (_args, value) => {
|
|
1200
|
+
if (value?.ok !== true || !value.xml) return [{ type: 'text', text: value?.error ?? '(dump failed)' }]
|
|
1201
|
+
const xml = value.xml
|
|
1202
|
+
const clipped = xml.length > 6000 ? xml.slice(0, 6000) + `\n… (truncated, full ${xml.length} chars)` : xml
|
|
1203
|
+
return [{ type: 'text', text: clipped }]
|
|
1204
|
+
},
|
|
1205
|
+
},
|
|
1206
|
+
async execute(args) {
|
|
1207
|
+
const devices = engine.deviceList()
|
|
1208
|
+
const serial = typeof args.serial === 'string' && args.serial !== ''
|
|
1209
|
+
? args.serial
|
|
1210
|
+
: devices.find((d) => d.state === 'device')?.serial ?? ''
|
|
1211
|
+
if (engine.adb === null) return { ok: false, error: 'adb not found on this host' }
|
|
1212
|
+
if (serial === '') return { ok: false, error: 'no attached device; connect and authorize one first' }
|
|
1213
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'uiautomator dump /sdcard/dsh-ui.xml >/dev/null 2>&1; cat /sdcard/dsh-ui.xml'], 30000)
|
|
1214
|
+
const xml = result.stdout.trim()
|
|
1215
|
+
if (result.ok && xml.startsWith('<?xml')) return { ok: true, xml }
|
|
1216
|
+
return { ok: false, error: (result.stderr || result.stdout || `dump failed (exit ${result.code ?? '?'})`).slice(0, 500) }
|
|
1217
|
+
},
|
|
1218
|
+
})
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** --------------------------------------------------- reverse-engineering */
|
|
1222
|
+
|
|
1223
|
+
/** The proc_list agent tool: structured process list. */
|
|
1224
|
+
function procListTool(engine) {
|
|
1225
|
+
return defineTool({
|
|
1226
|
+
name: 'proc_list',
|
|
1227
|
+
description: 'List running processes on the device (pid, ppid, user, name) — find a process by name before reading its maps / memory.',
|
|
1228
|
+
parameters: {
|
|
1229
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1230
|
+
filter: { type: 'string', description: 'Optional case-insensitive substring to filter process names, e.g. "game" or "com.example".' },
|
|
1231
|
+
},
|
|
1232
|
+
output: {
|
|
1233
|
+
schema: {
|
|
1234
|
+
type: 'object',
|
|
1235
|
+
additionalProperties: false,
|
|
1236
|
+
properties: {
|
|
1237
|
+
processes: {
|
|
1238
|
+
type: 'array',
|
|
1239
|
+
required: true,
|
|
1240
|
+
items: {
|
|
1241
|
+
type: 'object',
|
|
1242
|
+
additionalProperties: false,
|
|
1243
|
+
properties: {
|
|
1244
|
+
pid: { type: 'integer', required: true },
|
|
1245
|
+
ppid: { type: 'integer' },
|
|
1246
|
+
user: { type: 'string' },
|
|
1247
|
+
name: { type: 'string', required: true },
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
render: (_args, value) => {
|
|
1254
|
+
const list = value?.processes ?? []
|
|
1255
|
+
if (list.length === 0) return [{ type: 'text', text: '(no processes)' }]
|
|
1256
|
+
const lines = list.map((p) => `${String(p.pid).padStart(6)} ${(p.user ?? '?').padEnd(10)} ${p.name}`)
|
|
1257
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
async execute(args) {
|
|
1261
|
+
const devices = engine.deviceList()
|
|
1262
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
1263
|
+
if (serial === '' || engine.adb === null) return { processes: [] }
|
|
1264
|
+
const needle = (typeof args.filter === 'string' ? args.filter.trim() : '').toLowerCase()
|
|
1265
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'ps -A -o PID,PPID,USER,NAME'], 20000)
|
|
1266
|
+
const processes = []
|
|
1267
|
+
for (const line of (result.stdout ?? '').split(/\r?\n/)) {
|
|
1268
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s*$/.exec(line)
|
|
1269
|
+
if (m === null) continue
|
|
1270
|
+
const name = m[4]
|
|
1271
|
+
if (needle !== '' && !name.toLowerCase().includes(needle)) continue
|
|
1272
|
+
processes.push({ pid: Number.parseInt(m[1], 10), ppid: Number.parseInt(m[2], 10), user: m[3], name })
|
|
1273
|
+
}
|
|
1274
|
+
return { processes }
|
|
1275
|
+
},
|
|
1276
|
+
})
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** The proc_maps agent tool: memory maps + module bases (reverse engineering core). */
|
|
1280
|
+
function procMapsTool(engine) {
|
|
1281
|
+
return defineTool({
|
|
1282
|
+
name: 'proc_maps',
|
|
1283
|
+
description: 'Read /proc/<pid>/maps of a process and list memory-mapped modules (shared libraries, so bases) — ' +
|
|
1284
|
+
'the foundation for reverse engineering (find module base addresses, offsets, permissions). ' +
|
|
1285
|
+
'Note: reading other apps\' maps needs root or a debuggable/rooted device; own-process reads work via run-as on debuggable apps.',
|
|
1286
|
+
parameters: {
|
|
1287
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1288
|
+
pid: { type: 'integer', description: 'Process id (from proc_list).' },
|
|
1289
|
+
},
|
|
1290
|
+
output: {
|
|
1291
|
+
schema: {
|
|
1292
|
+
type: 'object',
|
|
1293
|
+
additionalProperties: false,
|
|
1294
|
+
properties: {
|
|
1295
|
+
pid: { type: 'integer', required: true },
|
|
1296
|
+
modules: {
|
|
1297
|
+
type: 'array',
|
|
1298
|
+
required: true,
|
|
1299
|
+
items: {
|
|
1300
|
+
type: 'object',
|
|
1301
|
+
additionalProperties: false,
|
|
1302
|
+
properties: {
|
|
1303
|
+
base: { type: 'string', required: true },
|
|
1304
|
+
end: { type: 'string' },
|
|
1305
|
+
perms: { type: 'string' },
|
|
1306
|
+
path: { type: 'string' },
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
error: { type: 'string' },
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
render: (_args, value) => {
|
|
1314
|
+
if (value?.error) return [{ type: 'text', text: value.error }]
|
|
1315
|
+
const mods = value?.modules ?? []
|
|
1316
|
+
if (mods.length === 0) return [{ type: 'text', text: `(pid ${value?.pid ?? '?'}: no file-backed mappings)` }]
|
|
1317
|
+
const lines = mods.map((m) => `${m.base}${m.end ? '-' + m.end : ''} ${(m.perms ?? '').padEnd(4)} ${m.path}`)
|
|
1318
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1321
|
+
async execute(args) {
|
|
1322
|
+
const devices = engine.deviceList()
|
|
1323
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
1324
|
+
const pid = Number(args.pid)
|
|
1325
|
+
if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, modules: [], error: 'a valid pid is required' }
|
|
1326
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', `cat /proc/${pid}/maps 2>&1`], 20000)
|
|
1327
|
+
if (!result.ok || result.stderr.includes('Permission denied') || /Permission denied|No such file/.test(result.stdout)) {
|
|
1328
|
+
return { pid, modules: [], error: `cannot read /proc/${pid}/maps (need root, or a debuggable app via run-as); output: ${(result.stdout || result.stderr).slice(0, 200)}` }
|
|
1329
|
+
}
|
|
1330
|
+
const seen = new Set()
|
|
1331
|
+
const modules = []
|
|
1332
|
+
for (const line of (result.stdout ?? '').split(/\r?\n/)) {
|
|
1333
|
+
const m = /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxps-]{4})\s+[0-9a-f]+\s+\S+\s+\d+\s+(.+)$/.exec(line)
|
|
1334
|
+
if (m === null) continue
|
|
1335
|
+
const path = m[4]
|
|
1336
|
+
if (!path.startsWith('/')) continue
|
|
1337
|
+
if (seen.has(path)) continue
|
|
1338
|
+
seen.add(path)
|
|
1339
|
+
modules.push({ base: m[1], end: m[2], perms: m[3], path })
|
|
1340
|
+
}
|
|
1341
|
+
return { pid, modules }
|
|
1342
|
+
},
|
|
1343
|
+
})
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/** The proc_status agent tool: /proc/<pid>/status summary + smaps totals. */
|
|
1347
|
+
function procStatusTool(engine) {
|
|
1348
|
+
return defineTool({
|
|
1349
|
+
name: 'proc_status',
|
|
1350
|
+
description: 'Read /proc/<pid>/status (state, uid, vm usage, threads) plus smaps Pss totals of a process — memory footprint analysis.',
|
|
1351
|
+
parameters: {
|
|
1352
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1353
|
+
pid: { type: 'integer', description: 'Process id (from proc_list).' },
|
|
1354
|
+
},
|
|
1355
|
+
output: {
|
|
1356
|
+
schema: {
|
|
1357
|
+
type: 'object',
|
|
1358
|
+
additionalProperties: false,
|
|
1359
|
+
properties: {
|
|
1360
|
+
pid: { type: 'integer', required: true },
|
|
1361
|
+
name: { type: 'string' },
|
|
1362
|
+
state: { type: 'string' },
|
|
1363
|
+
uid: { type: 'string' },
|
|
1364
|
+
vmSizeKb: { type: 'integer' },
|
|
1365
|
+
vmRssKb: { type: 'integer' },
|
|
1366
|
+
threads: { type: 'integer' },
|
|
1367
|
+
pssKb: { type: 'integer' },
|
|
1368
|
+
error: { type: 'string' },
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
render: (_args, value) => {
|
|
1372
|
+
if (value?.error) return [{ type: 'text', text: value.error }]
|
|
1373
|
+
const rows = ['name', 'state', 'uid', 'vmSizeKb', 'vmRssKb', 'threads', 'pssKb']
|
|
1374
|
+
.filter((k) => value?.[k] !== undefined)
|
|
1375
|
+
.map((k) => `${k}: ${value[k]}`)
|
|
1376
|
+
return [{ type: 'text', text: rows.length > 0 ? rows.join('\n') : `(pid ${value?.pid ?? '?'}: no data)` }]
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
async execute(args) {
|
|
1380
|
+
const devices = engine.deviceList()
|
|
1381
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
1382
|
+
const pid = Number(args.pid)
|
|
1383
|
+
if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, error: 'a valid pid is required' }
|
|
1384
|
+
const sh = `cat /proc/${pid}/status 2>&1; echo ===SMAPS===; awk '/^Pss:/ {s+=$2} END {print s}' /proc/${pid}/smaps 2>/dev/null; true`
|
|
1385
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
|
|
1386
|
+
if (!result.ok || result.stdout.trim() === '' || /No such file|No such process/.test(result.stdout)) {
|
|
1387
|
+
return { pid, error: `cannot read /proc/${pid} (need root or debuggable app); output: ${(result.stdout || result.stderr).slice(0, 200)}` }
|
|
1388
|
+
}
|
|
1389
|
+
const [statusText, , smapsOut] = result.stdout.split(/\r?\n===SMAPS===\r?\n|\n===SMAPS===\n/)
|
|
1390
|
+
const field = (k) => { const m = new RegExp(`^${k}:\\s*(.+)$`, 'm').exec(statusText ?? ''); return m ? m[1].trim() : undefined }
|
|
1391
|
+
const kb = (k) => { const v = field(k); return v ? Number.parseInt(v, 10) || undefined : undefined }
|
|
1392
|
+
return {
|
|
1393
|
+
pid,
|
|
1394
|
+
name: field('Name'),
|
|
1395
|
+
state: field('State'),
|
|
1396
|
+
uid: field('Uid'),
|
|
1397
|
+
vmSizeKb: kb('VmSize'),
|
|
1398
|
+
vmRssKb: kb('VmRSS'),
|
|
1399
|
+
threads: kb('Threads'),
|
|
1400
|
+
pssKb: Number.parseInt((smapsOut ?? '').trim(), 10) || undefined,
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1403
|
+
})
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** The mem_dump agent tool: read raw memory at an address (hex). */
|
|
1407
|
+
function memDumpTool(engine) {
|
|
1408
|
+
return defineTool({
|
|
1409
|
+
name: 'mem_dump',
|
|
1410
|
+
description: 'Dump raw memory of a process at a given address as hex (via /proc/<pid>/mem) — for reverse engineering. ' +
|
|
1411
|
+
'Requires root on most devices (or a debuggable app via run-as). Address in hex, length in bytes (max 65536, default 256).',
|
|
1412
|
+
parameters: {
|
|
1413
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1414
|
+
pid: { type: 'integer', description: 'Process id (from proc_list).' },
|
|
1415
|
+
address: { type: 'string', description: 'Start address in hex, e.g. "0x7a0000" or "7a0000".' },
|
|
1416
|
+
length: { type: 'integer', description: 'Bytes to read (default 256, max 65536).' },
|
|
1417
|
+
},
|
|
1418
|
+
output: {
|
|
1419
|
+
schema: {
|
|
1420
|
+
type: 'object',
|
|
1421
|
+
additionalProperties: false,
|
|
1422
|
+
properties: {
|
|
1423
|
+
pid: { type: 'integer', required: true },
|
|
1424
|
+
address: { type: 'string' },
|
|
1425
|
+
hex: { type: 'string' },
|
|
1426
|
+
error: { type: 'string' },
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
render: (_args, value) => {
|
|
1430
|
+
if (value?.error) return [{ type: 'text', text: value.error }]
|
|
1431
|
+
return [{ type: 'text', text: value?.hex ? `pid ${value.pid} @ ${value.address}:\n${value.hex}` : '(no data)' }]
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
async execute(args) {
|
|
1435
|
+
const devices = engine.deviceList()
|
|
1436
|
+
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
1437
|
+
const pid = Number(args.pid)
|
|
1438
|
+
const rawAddr = typeof args.address === 'string' ? args.address.trim() : ''
|
|
1439
|
+
const addr = Number.parseInt(rawAddr.replace(/^0x/i, ''), 16)
|
|
1440
|
+
const len = Math.min(Math.max(Number(args.length ?? 256) || 256, 1), 65536)
|
|
1441
|
+
if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, error: 'a valid pid is required' }
|
|
1442
|
+
if (!Number.isFinite(addr) || addr < 0) return { pid, error: `invalid address: ${rawAddr}` }
|
|
1443
|
+
const sh = `dd if=/proc/${pid}/mem bs=1 skip=${addr} count=${len} of=/sdcard/dsh-mem.bin 2>/dev/null && od -A x -t x1 -v /sdcard/dsh-mem.bin && rm /sdcard/dsh-mem.bin || echo DSH_MEM_FAIL`
|
|
1444
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
|
|
1445
|
+
if (!result.ok || result.stdout.includes('DSH_MEM_FAIL') || result.stdout.trim() === '') {
|
|
1446
|
+
return { pid, error: `cannot read memory at ${rawAddr} (need root / debuggable app); output: ${(result.stdout + result.stderr).slice(0, 200)}` }
|
|
1447
|
+
}
|
|
1448
|
+
return { pid, address: '0x' + addr.toString(16), hex: result.stdout.trim() }
|
|
1449
|
+
},
|
|
1450
|
+
})
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** The frida_server agent tool: deploy / start / stop frida-server. */
|
|
1454
|
+
function fridaServerTool(engine) {
|
|
1455
|
+
return defineTool({
|
|
1456
|
+
name: 'frida_server',
|
|
1457
|
+
description: 'Manage frida-server on the device for dynamic instrumentation (reverse engineering): ' +
|
|
1458
|
+
'status (is it deployed/running), start (push a local frida-server binary and run it), stop (kill it). ' +
|
|
1459
|
+
'The frida-server binary must match the device ABI (arm64/arm/x86) — get it from the frida releases page.',
|
|
1460
|
+
parameters: {
|
|
1461
|
+
serial: { type: 'string', description: 'Device serial from logcat_devices (optional; defaults to the first attached device).' },
|
|
1462
|
+
action: { type: 'string', enum: ['status', 'start', 'stop'], description: 'What to do: status / start / stop.' },
|
|
1463
|
+
localPath: { type: 'string', description: 'Absolute local path of the frida-server binary (required for start), e.g. "F:/tools/frida-server-16.5.5-android-arm64".' },
|
|
1464
|
+
},
|
|
1465
|
+
output: {
|
|
1466
|
+
schema: {
|
|
1467
|
+
type: 'object',
|
|
1468
|
+
additionalProperties: false,
|
|
1469
|
+
properties: {
|
|
1470
|
+
ok: { type: 'boolean', required: true },
|
|
1471
|
+
running: { type: 'boolean' },
|
|
1472
|
+
pid: { type: 'integer' },
|
|
1473
|
+
output: { type: 'string' },
|
|
1474
|
+
error: { type: 'string' },
|
|
1475
|
+
},
|
|
1476
|
+
},
|
|
1477
|
+
render: (_args, value) => {
|
|
1478
|
+
if (value?.error) return [{ type: 'text', text: value.error }]
|
|
1479
|
+
const parts = []
|
|
1480
|
+
if (value?.running !== undefined) parts.push(`running: ${value.running}${value.pid ? ` (pid ${value.pid})` : ''}`)
|
|
1481
|
+
if (value?.output) parts.push(value.output.trimEnd())
|
|
1482
|
+
return [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : `ok: ${value?.ok}` }]
|
|
1483
|
+
},
|
|
1484
|
+
},
|
|
1485
|
+
async execute(args) {
|
|
1486
|
+
const devices = engine.deviceList()
|
|
1487
|
+
const serial = typeof args.serial === 'string' && args.serial !== ''
|
|
1488
|
+
? args.serial
|
|
1489
|
+
: devices.find((d) => d.state === 'device')?.serial ?? ''
|
|
1490
|
+
const action = typeof args.action === 'string' ? args.action : 'status'
|
|
1491
|
+
if (engine.adb === null) return { ok: false, error: 'adb not found on this host' }
|
|
1492
|
+
if (serial === '') return { ok: false, error: 'no attached device; connect and authorize one first' }
|
|
1493
|
+
if (action === 'start') {
|
|
1494
|
+
const localPath = typeof args.localPath === 'string' ? args.localPath.trim() : ''
|
|
1495
|
+
if (localPath === '' || !existsSync(localPath)) return { ok: false, error: `frida-server binary not found: ${localPath || '(none given)'}` }
|
|
1496
|
+
const push = await runAdbFull(engine.adb, ['-s', serial, 'push', localPath, '/data/local/tmp/frida-server'], 120000)
|
|
1497
|
+
if (!push.ok) return { ok: false, error: `push failed: ${(push.stderr || push.stdout).slice(0, 200)}` }
|
|
1498
|
+
await runAdbFull(engine.adb, ['-s', serial, 'shell', 'chmod 755 /data/local/tmp/frida-server'], 10000)
|
|
1499
|
+
const start = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'nohup /data/local/tmp/frida-server >/dev/null 2>&1 &'], 10000)
|
|
1500
|
+
if (!start.ok) return { ok: false, error: `start failed: ${(start.stderr || start.stdout).slice(0, 200)}` }
|
|
1501
|
+
const check = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'ps -A | grep frida-server | head -1'], 10000)
|
|
1502
|
+
const m = /^\s*(\d+)/.exec(check.stdout ?? '')
|
|
1503
|
+
return { ok: true, running: m !== null, pid: m ? Number.parseInt(m[1], 10) : undefined, output: 'frida-server started' }
|
|
1504
|
+
}
|
|
1505
|
+
if (action === 'stop') {
|
|
1506
|
+
const stop = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'pkill -f frida-server; echo stopped'], 10000)
|
|
1507
|
+
return { ok: stop.ok, running: false, output: 'frida-server stopped' }
|
|
1508
|
+
}
|
|
1509
|
+
// status
|
|
1510
|
+
const exists = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'ls -l /data/local/tmp/frida-server 2>&1'], 10000)
|
|
1511
|
+
const running = await runAdbFull(engine.adb, ['-s', serial, 'shell', 'ps -A | grep frida-server | head -1'], 10000)
|
|
1512
|
+
const m = /^\s*(\d+)/.exec(running.stdout ?? '')
|
|
1513
|
+
return {
|
|
1514
|
+
ok: true,
|
|
1515
|
+
running: m !== null,
|
|
1516
|
+
pid: m ? Number.parseInt(m[1], 10) : undefined,
|
|
1517
|
+
output: `deployed: ${exists.ok ? 'yes' : 'no'}\n` + ((running.stdout ?? '').trim() || 'not running'),
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
})
|
|
1521
|
+
}
|
|
1522
|
+
|
|
914
1523
|
/** ------------------------------------------------------------------ */
|
|
915
1524
|
|
|
916
1525
|
/** Mount the adb engine, routes, tool, and announcement. */
|
|
@@ -982,6 +1591,18 @@ export function apply(ctx, config) {
|
|
|
982
1591
|
logcatSetPackageTool(engine),
|
|
983
1592
|
adbInstallTool(engine),
|
|
984
1593
|
adbPullTool(engine),
|
|
1594
|
+
logcatCrashTool(engine),
|
|
1595
|
+
deviceInfoTool(engine),
|
|
1596
|
+
deviceStatsTool(engine),
|
|
1597
|
+
inputTapTool(engine),
|
|
1598
|
+
inputSwipeTool(engine),
|
|
1599
|
+
inputTextTool(engine),
|
|
1600
|
+
uiDumpTool(engine),
|
|
1601
|
+
procListTool(engine),
|
|
1602
|
+
procMapsTool(engine),
|
|
1603
|
+
procStatusTool(engine),
|
|
1604
|
+
memDumpTool(engine),
|
|
1605
|
+
fridaServerTool(engine),
|
|
985
1606
|
].map((tool) => ctx.tools.register(tool))
|
|
986
1607
|
return () => { for (const dispose of disposers) dispose() }
|
|
987
1608
|
}, 'dsh-logcat: tools')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windypro-rourou/dsh-logcat",
|
|
3
3
|
"description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|