@windypro-rourou/dsh-logcat 0.6.2 → 0.6.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/lib/client.js +65 -6
- package/lib/index.js +45 -15
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -133,6 +133,8 @@ window.__ModuleLoader__.load({
|
|
|
133
133
|
.lc-re-row[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.2)); }
|
|
134
134
|
.lc-re-dump { flex: 1; overflow: auto; padding: 8px 10px; font-family: Consolas, "Cascadia Mono", monospace; font-size: 11px; line-height: 16px; white-space: pre-wrap; word-break: break-all; }
|
|
135
135
|
.lc-re-err { padding: 8px 10px; color: #ef5350; font-size: 12px; white-space: pre-wrap; }
|
|
136
|
+
.lc-re-man { display: flex; gap: 6px; padding: 6px 10px; align-items: center; flex-wrap: wrap; }
|
|
137
|
+
.lc-re-man input { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
|
|
136
138
|
.lc-screen { position: absolute; inset: 0; display: flex; flex-direction: column; }
|
|
137
139
|
.lc-screen-bar { display: flex; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); flex: none; align-items: center; flex-wrap: wrap; font-size: 11px; }
|
|
138
140
|
.lc-screen-bar input { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 4px 8px; }
|
|
@@ -226,6 +228,10 @@ window.__ModuleLoader__.load({
|
|
|
226
228
|
const [reDump, setReDump] = useState("");
|
|
227
229
|
const [reDumpAddr, setReDumpAddr] = useState("");
|
|
228
230
|
const [reError, setReError] = useState("");
|
|
231
|
+
const [reMaps, setReMaps] = useState([]);
|
|
232
|
+
const [reMapError, setReMapError] = useState("");
|
|
233
|
+
const [reManualAddr, setReManualAddr] = useState("");
|
|
234
|
+
const [reDumpLen, setReDumpLen] = useState(256);
|
|
229
235
|
const [spark, setSpark] = useState({ cpu: [], mem: [], bat: [] });
|
|
230
236
|
const [screenImg, setScreenImg] = useState(null);
|
|
231
237
|
const [screenText, setScreenText] = useState("");
|
|
@@ -727,20 +733,57 @@ window.__ModuleLoader__.load({
|
|
|
727
733
|
.finally(() => setReSearching(false));
|
|
728
734
|
};
|
|
729
735
|
|
|
730
|
-
|
|
731
|
-
|
|
736
|
+
// od hex text -> aligned hex + ASCII columns for readability.
|
|
737
|
+
const formatHexDump = (text) => {
|
|
738
|
+
if (text === "") return "";
|
|
739
|
+
if (/DSH_MEM_FAIL|Permission denied|No such file/i.test(text)) return text;
|
|
740
|
+
const out = [];
|
|
741
|
+
for (const line of text.split(/\r?\n/)) {
|
|
742
|
+
const m = /^([0-9a-f]+):\s+(.*)$/.exec(line.trim());
|
|
743
|
+
if (m === null) { out.push(line); continue; }
|
|
744
|
+
const bytes = (m[2].match(/[0-9a-f]{2}/gi) ?? []).map((b) => Number.parseInt(b, 16));
|
|
745
|
+
const ascii = bytes.map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
|
|
746
|
+
out.push(m[1].padStart(7, "0") + " " + m[2].padEnd(48) + " |" + ascii + "|");
|
|
747
|
+
}
|
|
748
|
+
return out.join("\n");
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const reDumpAt = (addr, len) => {
|
|
752
|
+
const hexAddr = typeof addr === "string" && addr.startsWith("0x") ? addr : "0x" + addr.toString(16);
|
|
753
|
+
const length = Math.min(Math.max(Number(len ?? 256) || 256, 1), 65536);
|
|
732
754
|
setReDumpAddr(hexAddr);
|
|
733
755
|
setReDump("读取中…");
|
|
734
756
|
setReError("");
|
|
735
|
-
fetch(API_BASE + "/mem-dump?serial=" + encodeURIComponent(serial) + "&pid=" + rePid + "&address=" + encodeURIComponent(hexAddr) + "&length=
|
|
757
|
+
fetch(API_BASE + "/mem-dump?serial=" + encodeURIComponent(serial) + "&pid=" + rePid + "&address=" + encodeURIComponent(hexAddr) + "&length=" + length)
|
|
736
758
|
.then((res) => res.json())
|
|
737
759
|
.then((body) => {
|
|
738
760
|
if (body?.error) { setReError(body.error); setReDump(""); return; }
|
|
739
|
-
setReDump(body?.hex ?? "");
|
|
761
|
+
setReDump(formatHexDump(body?.hex ?? ""));
|
|
740
762
|
})
|
|
741
763
|
.catch(() => { setReDump(""); setReError("转储失败"); });
|
|
742
764
|
};
|
|
743
765
|
|
|
766
|
+
// Load the selected process's memory-mapped modules (/proc/<pid>/maps).
|
|
767
|
+
const reFetchMaps = (pid) => {
|
|
768
|
+
const target = Number.isInteger(pid) ? pid : rePid;
|
|
769
|
+
if (!Number.isInteger(target) || target <= 0) return;
|
|
770
|
+
setReMaps([]);
|
|
771
|
+
setReMapError("");
|
|
772
|
+
fetch(API_BASE + "/maps?serial=" + encodeURIComponent(serial) + "&pid=" + target)
|
|
773
|
+
.then((res) => res.json())
|
|
774
|
+
.then((body) => {
|
|
775
|
+
if (body?.error) { setReMapError(body.error); setReMaps([]); return; }
|
|
776
|
+
setReMaps(body?.modules ?? []);
|
|
777
|
+
})
|
|
778
|
+
.catch(() => setReMapError("模块列表加载失败"));
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const manualDump = () => {
|
|
782
|
+
const addr = reManualAddr.trim().replace(/^0x/i, "");
|
|
783
|
+
if (addr === "" || !/^[0-9a-fA-F]+$/.test(addr)) { setReError("地址格式:0x7a0000 或 7a0000"); return; }
|
|
784
|
+
reDumpAt("0x" + addr.toLowerCase(), reDumpLen);
|
|
785
|
+
};
|
|
786
|
+
|
|
744
787
|
// Live screen stream: on while the 屏幕 tab is open, the panel is open, and a device is selected.
|
|
745
788
|
useEffect(() => {
|
|
746
789
|
const prev = prevSerialRef.current;
|
|
@@ -1025,16 +1068,32 @@ window.__ModuleLoader__.load({
|
|
|
1025
1068
|
rePids.length === 0
|
|
1026
1069
|
? h("div", { className: "lc-re-row", title: "点击「进程」或等待自动加载" }, "(尚未加载 — 点击「进程」刷新)")
|
|
1027
1070
|
: rePids.map((p) =>
|
|
1028
|
-
h("div", { key: p.pid, className: "lc-re-row", "data-on": rePid === p.pid ? "" : undefined, title: (p.user ?? "") + " " + p.name, onClick: () => { setRePid(p.pid); setReResults([]); setReDump(""); setReError(""); } },
|
|
1071
|
+
h("div", { key: p.pid, className: "lc-re-row", "data-on": rePid === p.pid ? "" : undefined, title: (p.user ?? "") + " " + p.name, onClick: () => { setRePid(p.pid); setReResults([]); setReDump(""); setReError(""); reFetchMaps(p.pid); } },
|
|
1029
1072
|
p.pid + " " + p.name)))),
|
|
1030
1073
|
h("div", { className: "lc-re-col2" },
|
|
1074
|
+
h("div", { className: "lc-re-head" }, "模块 / so 库 (" + reMaps.length + ") — 点击转储基址"),
|
|
1075
|
+
h("div", { className: "lc-re-list" },
|
|
1076
|
+
reMapError !== ""
|
|
1077
|
+
? h("div", { className: "lc-re-row" }, reMapError)
|
|
1078
|
+
: reMaps.length === 0
|
|
1079
|
+
? h("div", { className: "lc-re-row" }, "(无模块 — 普通设备无法读取其他应用 maps,需 root)")
|
|
1080
|
+
: reMaps.map((mo) =>
|
|
1081
|
+
h("div", { key: mo.path, className: "lc-re-row", title: mo.path + " [" + mo.perms + "]", onClick: () => reDumpAt(mo.base, 256) },
|
|
1082
|
+
(mo.path.split("/").pop() ?? mo.path) + " @" + mo.base + " " + mo.perms))),
|
|
1031
1083
|
h("div", { className: "lc-re-head" }, "匹配地址(点击转储 256B)" + (reResults.length > 0 ? " · " + reResults.length : "")),
|
|
1032
1084
|
h("div", { className: "lc-re-list" },
|
|
1033
1085
|
reResults.map((o) =>
|
|
1034
1086
|
h("div", { key: o, className: "lc-re-row", "data-on": reDumpAddr === "0x" + o.toString(16) ? "" : undefined, title: "0x" + o.toString(16), onClick: () => reDumpAt(o) },
|
|
1035
1087
|
"0x" + o.toString(16)))),
|
|
1088
|
+
h("div", { className: "lc-re-head" }, "手动转储"),
|
|
1089
|
+
h("div", { className: "lc-re-man" },
|
|
1090
|
+
h("input", { placeholder: "地址 0x7a0000…", value: reManualAddr, onChange: (e) => setReManualAddr(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") manualDump(); }, style: { flex: 1, minWidth: 120 } }),
|
|
1091
|
+
h("select", { className: "lc-select", value: String(reDumpLen), onChange: (e) => setReDumpLen(Number(e.target.value)) },
|
|
1092
|
+
[128, 256, 1024, 4096].map((n) => h("option", { key: n, value: String(n) }, n + "B"))),
|
|
1093
|
+
h("button", { type: "button", className: "lc-btn", onClick: manualDump }, "转储"),
|
|
1094
|
+
),
|
|
1036
1095
|
reDump !== ""
|
|
1037
|
-
? h("div", { className: "lc-re-head" }, "转储 " + reDumpAddr)
|
|
1096
|
+
? h("div", { className: "lc-re-head" }, "转储 " + reDumpAddr + " · " + reDumpLen + "B")
|
|
1038
1097
|
: null,
|
|
1039
1098
|
reDump !== "" ? h("div", { className: "lc-re-dump" }, reDump) : null,
|
|
1040
1099
|
reError !== "" ? h("div", { className: "lc-re-err" }, reError) : null,
|
package/lib/index.js
CHANGED
|
@@ -1275,6 +1275,26 @@ function makeRoutes(engine) {
|
|
|
1275
1275
|
}
|
|
1276
1276
|
},
|
|
1277
1277
|
},
|
|
1278
|
+
{
|
|
1279
|
+
kind: 'exact',
|
|
1280
|
+
path: API_BASE + '/maps',
|
|
1281
|
+
handler: async (req, res) => {
|
|
1282
|
+
if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
|
|
1283
|
+
if ((req.method ?? 'GET') !== 'GET') { writeJson(res, 405, { error: 'method not allowed' }); return }
|
|
1284
|
+
if (engine.adb === null) { writeJson(res, 500, { error: 'adb not found' }); return }
|
|
1285
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
1286
|
+
const serial = url.searchParams.get('serial') ?? ''
|
|
1287
|
+
const pid = Number(url.searchParams.get('pid'))
|
|
1288
|
+
if (serial === '' || !engine.devices.has(serial) || !Number.isInteger(pid) || pid <= 0) {
|
|
1289
|
+
writeJson(res, 400, { error: 'serial + valid pid required' }); return
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
writeJson(res, 200, { pid, modules: await listModules(engine, serial, pid) })
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
writeJson(res, 200, { pid, modules: [], error: error instanceof Error ? error.message : String(error) })
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1278
1298
|
]
|
|
1279
1299
|
|
|
1280
1300
|
const upgrade = {
|
|
@@ -1966,6 +1986,26 @@ function procListTool(engine) {
|
|
|
1966
1986
|
})
|
|
1967
1987
|
}
|
|
1968
1988
|
|
|
1989
|
+
/** Read /proc/<pid>/maps and return unique file-backed module mappings (base/end/perms/path). Throws on permission failure. */
|
|
1990
|
+
export async function listModules(engine, serial, pid) {
|
|
1991
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', `cat /proc/${pid}/maps 2>&1`], 20000)
|
|
1992
|
+
if (!result.ok || result.stderr.includes('Permission denied') || /Permission denied|No such file/.test(result.stdout)) {
|
|
1993
|
+
throw new Error(`cannot read /proc/${pid}/maps (need root, or a debuggable app via run-as); output: ${(result.stdout || result.stderr).slice(0, 200)}`)
|
|
1994
|
+
}
|
|
1995
|
+
const seen = new Set()
|
|
1996
|
+
const modules = []
|
|
1997
|
+
for (const line of (result.stdout ?? '').split(/\r?\n/)) {
|
|
1998
|
+
const m = /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxps-]{4})\s+[0-9a-f]+\s+\S+\s+\d+\s+(.+)$/.exec(line)
|
|
1999
|
+
if (m === null) continue
|
|
2000
|
+
const path = m[4]
|
|
2001
|
+
if (!path.startsWith('/')) continue
|
|
2002
|
+
if (seen.has(path)) continue
|
|
2003
|
+
seen.add(path)
|
|
2004
|
+
modules.push({ base: m[1], end: m[2], perms: m[3], path })
|
|
2005
|
+
}
|
|
2006
|
+
return modules
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1969
2009
|
/** The proc_maps agent tool: memory maps + module bases (reverse engineering core). */
|
|
1970
2010
|
function procMapsTool(engine) {
|
|
1971
2011
|
return defineTool({
|
|
@@ -2013,22 +2053,12 @@ function procMapsTool(engine) {
|
|
|
2013
2053
|
const serial = typeof args.serial === 'string' && args.serial !== '' ? args.serial : devices[0]?.serial ?? ''
|
|
2014
2054
|
const pid = Number(args.pid)
|
|
2015
2055
|
if (serial === '' || engine.adb === null || !Number.isInteger(pid) || pid <= 0) return { pid: 0, modules: [], error: 'a valid pid is required' }
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
return { pid, modules
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
const modules = []
|
|
2022
|
-
for (const line of (result.stdout ?? '').split(/\r?\n/)) {
|
|
2023
|
-
const m = /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxps-]{4})\s+[0-9a-f]+\s+\S+\s+\d+\s+(.+)$/.exec(line)
|
|
2024
|
-
if (m === null) continue
|
|
2025
|
-
const path = m[4]
|
|
2026
|
-
if (!path.startsWith('/')) continue
|
|
2027
|
-
if (seen.has(path)) continue
|
|
2028
|
-
seen.add(path)
|
|
2029
|
-
modules.push({ base: m[1], end: m[2], perms: m[3], path })
|
|
2056
|
+
try {
|
|
2057
|
+
const modules = await listModules(engine, serial, pid)
|
|
2058
|
+
return { pid, modules }
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
return { pid, modules: [], error: error instanceof Error ? error.message : String(error) }
|
|
2030
2061
|
}
|
|
2031
|
-
return { pid, modules }
|
|
2032
2062
|
},
|
|
2033
2063
|
})
|
|
2034
2064
|
}
|
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.6.
|
|
4
|
+
"version": "0.6.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|