@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 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
- const reDumpAt = (addr) => {
731
- const hexAddr = addr.startsWith("0x") ? addr : "0x" + addr.toString(16);
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=256")
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
- const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', `cat /proc/${pid}/maps 2>&1`], 20000)
2017
- if (!result.ok || result.stderr.includes('Permission denied') || /Permission denied|No such file/.test(result.stdout)) {
2018
- 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)}` }
2019
- }
2020
- const seen = new Set()
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.2",
4
+ "version": "0.6.3",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {