@windypro-rourou/dsh-logcat 0.6.2 → 0.6.4

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.
Files changed (3) hide show
  1. package/lib/client.js +145 -29
  2. package/lib/index.js +45 -15
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -61,6 +61,11 @@ window.__ModuleLoader__.load({
61
61
  .lc-mem-bar { display: inline-block; width: 44px; height: 5px; border-radius: 3px; background: light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14)); overflow: hidden; vertical-align: middle; }
62
62
  .lc-mem-fill { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, #4caf50, #fbc02d); }
63
63
  .lc-mem-fill.hot { background: linear-gradient(90deg, #fbc02d, #ef5350); }
64
+ .lc-icobtn { border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); background: transparent; color: inherit; font: inherit; font-size: 13px; width: 30px; height: 26px; padding: 0; border-radius: 7px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
65
+ .lc-icobtn:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
66
+ .lc-icobtn[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); border-color: rgba(66,133,244,.45); color: #4285f4; }
67
+ .lc-hit { background: rgba(66,133,244,.28); border-radius: 2px; color: inherit; }
68
+ .lc-screen-img:fullscreen { background: #000; }
64
69
  .lc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; flex-wrap: wrap; }
65
70
  .lc-select, .lc-search { 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: 5px 9px; }
66
71
  .lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
@@ -133,6 +138,8 @@ window.__ModuleLoader__.load({
133
138
  .lc-re-row[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.2)); }
134
139
  .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
140
  .lc-re-err { padding: 8px 10px; color: #ef5350; font-size: 12px; white-space: pre-wrap; }
141
+ .lc-re-man { display: flex; gap: 6px; padding: 6px 10px; align-items: center; flex-wrap: wrap; }
142
+ .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
143
  .lc-screen { position: absolute; inset: 0; display: flex; flex-direction: column; }
137
144
  .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
145
  .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; }
@@ -201,7 +208,8 @@ window.__ModuleLoader__.load({
201
208
  const [keyword, setKeyword] = useState("");
202
209
  const [autoScroll, setAutoScroll] = useState(true);
203
210
  const [scrollTop, setScrollTop] = useState(0);
204
- const [tab, setTab] = useState("log");
211
+ const [tab, setTab] = useState(() => { try { const t = localStorage.getItem("dsh-logcat-tab"); return t === "screen" || t === "re" ? t : "log"; } catch { return "log"; } });
212
+ const switchTab = (t) => { setTab(t); try { localStorage.setItem("dsh-logcat-tab", t); } catch { /* private mode */ } };
205
213
  const [eventsOn, setEventsOn] = useState(false);
206
214
  const [eventsEntries, setEventsEntries] = useState([]);
207
215
  const [eventsStreaming, setEventsStreaming] = useState([]);
@@ -226,6 +234,10 @@ window.__ModuleLoader__.load({
226
234
  const [reDump, setReDump] = useState("");
227
235
  const [reDumpAddr, setReDumpAddr] = useState("");
228
236
  const [reError, setReError] = useState("");
237
+ const [reMaps, setReMaps] = useState([]);
238
+ const [reMapError, setReMapError] = useState("");
239
+ const [reManualAddr, setReManualAddr] = useState("");
240
+ const [reDumpLen, setReDumpLen] = useState(256);
229
241
  const [spark, setSpark] = useState({ cpu: [], mem: [], bat: [] });
230
242
  const [screenImg, setScreenImg] = useState(null);
231
243
  const [screenText, setScreenText] = useState("");
@@ -544,6 +556,24 @@ window.__ModuleLoader__.load({
544
556
  }
545
557
  };
546
558
 
559
+ // Append a tag/term to the keyword filter (matches any term).
560
+ const addKeyword = (term) => {
561
+ const t = (term ?? "").trim();
562
+ if (t === "") return;
563
+ setKeyword((prev) => {
564
+ const terms = prev.trim().toLowerCase().split(/\s+/).filter(Boolean);
565
+ if (terms.includes(t.toLowerCase())) return prev;
566
+ return prev.trim() === "" ? t : prev.trim() + " " + t;
567
+ });
568
+ };
569
+
570
+ // Copy one log line (double-click).
571
+ const copyLine = (raw) => {
572
+ if (navigator.clipboard !== undefined && raw !== "") {
573
+ navigator.clipboard.writeText(raw).catch(() => { /* denied */ });
574
+ }
575
+ };
576
+
547
577
  const device = devices.find((d) => d.serial === serial);
548
578
  const deviceState = device?.state ?? "";
549
579
  const live = connected && serial !== "" && streaming.includes(serial);
@@ -727,20 +757,57 @@ window.__ModuleLoader__.load({
727
757
  .finally(() => setReSearching(false));
728
758
  };
729
759
 
730
- const reDumpAt = (addr) => {
731
- const hexAddr = addr.startsWith("0x") ? addr : "0x" + addr.toString(16);
760
+ // od hex text -> aligned hex + ASCII columns for readability.
761
+ const formatHexDump = (text) => {
762
+ if (text === "") return "";
763
+ if (/DSH_MEM_FAIL|Permission denied|No such file/i.test(text)) return text;
764
+ const out = [];
765
+ for (const line of text.split(/\r?\n/)) {
766
+ const m = /^([0-9a-f]+):\s+(.*)$/.exec(line.trim());
767
+ if (m === null) { out.push(line); continue; }
768
+ const bytes = (m[2].match(/[0-9a-f]{2}/gi) ?? []).map((b) => Number.parseInt(b, 16));
769
+ const ascii = bytes.map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
770
+ out.push(m[1].padStart(7, "0") + " " + m[2].padEnd(48) + " |" + ascii + "|");
771
+ }
772
+ return out.join("\n");
773
+ };
774
+
775
+ const reDumpAt = (addr, len) => {
776
+ const hexAddr = typeof addr === "string" && addr.startsWith("0x") ? addr : "0x" + addr.toString(16);
777
+ const length = Math.min(Math.max(Number(len ?? 256) || 256, 1), 65536);
732
778
  setReDumpAddr(hexAddr);
733
779
  setReDump("读取中…");
734
780
  setReError("");
735
- fetch(API_BASE + "/mem-dump?serial=" + encodeURIComponent(serial) + "&pid=" + rePid + "&address=" + encodeURIComponent(hexAddr) + "&length=256")
781
+ fetch(API_BASE + "/mem-dump?serial=" + encodeURIComponent(serial) + "&pid=" + rePid + "&address=" + encodeURIComponent(hexAddr) + "&length=" + length)
736
782
  .then((res) => res.json())
737
783
  .then((body) => {
738
784
  if (body?.error) { setReError(body.error); setReDump(""); return; }
739
- setReDump(body?.hex ?? "");
785
+ setReDump(formatHexDump(body?.hex ?? ""));
740
786
  })
741
787
  .catch(() => { setReDump(""); setReError("转储失败"); });
742
788
  };
743
789
 
790
+ // Load the selected process's memory-mapped modules (/proc/<pid>/maps).
791
+ const reFetchMaps = (pid) => {
792
+ const target = Number.isInteger(pid) ? pid : rePid;
793
+ if (!Number.isInteger(target) || target <= 0) return;
794
+ setReMaps([]);
795
+ setReMapError("");
796
+ fetch(API_BASE + "/maps?serial=" + encodeURIComponent(serial) + "&pid=" + target)
797
+ .then((res) => res.json())
798
+ .then((body) => {
799
+ if (body?.error) { setReMapError(body.error); setReMaps([]); return; }
800
+ setReMaps(body?.modules ?? []);
801
+ })
802
+ .catch(() => setReMapError("模块列表加载失败"));
803
+ };
804
+
805
+ const manualDump = () => {
806
+ const addr = reManualAddr.trim().replace(/^0x/i, "");
807
+ if (addr === "" || !/^[0-9a-fA-F]+$/.test(addr)) { setReError("地址格式:0x7a0000 或 7a0000"); return; }
808
+ reDumpAt("0x" + addr.toLowerCase(), reDumpLen);
809
+ };
810
+
744
811
  // Live screen stream: on while the 屏幕 tab is open, the panel is open, and a device is selected.
745
812
  useEffect(() => {
746
813
  const prev = prevSerialRef.current;
@@ -862,6 +929,13 @@ window.__ModuleLoader__.load({
862
929
  } catch { /* empty canvas */ }
863
930
  };
864
931
 
932
+ const toggleScreenFullscreen = () => {
933
+ const el = screenWrapRef.current;
934
+ if (el === null) return;
935
+ if (document.fullscreenElement !== null) { try { document.exitFullscreen(); } catch { /* ignored */ } return; }
936
+ try { el.requestFullscreen?.(); } catch { /* denied */ }
937
+ };
938
+
865
939
  return h("div", { className: "lc-panel" },
866
940
  h("div", { className: "lc-header" },
867
941
  h("button", { type: "button", className: "lc-back", onClick: () => controller.close() },
@@ -915,9 +989,9 @@ window.__ModuleLoader__.load({
915
989
  stats === null && serial !== "" ? h("span", { className: "lc-chip" }, "正在采集设备状态…") : null,
916
990
  ),
917
991
  h("div", { className: "lc-tabs" },
918
- h("button", { type: "button", className: "lc-tab", "data-on": tab === "log" ? "" : undefined, onClick: () => setTab("log") }, "日志"),
919
- h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () => setTab("screen") }, "屏幕"),
920
- h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () => setTab("re") }, "逆向"),
992
+ h("button", { type: "button", className: "lc-tab", "data-on": tab === "log" ? "" : undefined, onClick: () => switchTab("log") }, "日志"),
993
+ h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () => switchTab("screen") }, "屏幕"),
994
+ h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () => switchTab("re") }, "逆向"),
921
995
  ),
922
996
  tab === "log"
923
997
  ? h("div", { className: "lc-toolbar" },
@@ -957,11 +1031,11 @@ window.__ModuleLoader__.load({
957
1031
  style: { maxWidth: 180 },
958
1032
  }),
959
1033
  h("button", { type: "button", className: "lc-btn", onClick: () => setPackageFilter(pkgInput), title: "按包名过滤日志(agent 侧 logcat_recent 同步生效)" }, "包名"),
960
- h("button", { type: "button", className: "lc-btn", onClick: takeScreenshot, title: "截取真机屏幕并下载 PNG" }, "截图"),
961
- h("button", { type: "button", className: "lc-btn", onClick: loadHistory, title: "从磁盘加载历史日志(重启不丢)" }, "历史"),
962
- h("button", { type: "button", className: "lc-btn", "data-on": eventsOn ? "" : undefined, onClick: toggleEvents, title: "logcat -b events 事件缓冲(应用启动/崩溃/生命周期)" }, "events"),
963
- h("button", { type: "button", className: "lc-btn", onClick: openCrashModal, title: "崩溃自动快照历史(截图+上下文)" }, "崩溃"),
964
- h("button", { type: "button", className: "lc-btn", onClick: () => setWifiOpen(true), title: "WiFi 无线调试(adb pair + connect)" }, "WiFi"),
1034
+ h("button", { type: "button", className: "lc-icobtn", onClick: takeScreenshot, title: "截取真机屏幕并下载 PNG" }, "📷"),
1035
+ h("button", { type: "button", className: "lc-icobtn", onClick: loadHistory, title: "从磁盘加载历史日志(重启不丢)" }, "🕘"),
1036
+ h("button", { type: "button", className: "lc-icobtn", "data-on": eventsOn ? "" : undefined, onClick: toggleEvents, title: "logcat -b events 事件缓冲(应用启动/崩溃/生命周期)" }, "📑"),
1037
+ h("button", { type: "button", className: "lc-icobtn", onClick: openCrashModal, title: "崩溃自动快照历史(截图+上下文)" }, "💥"),
1038
+ h("button", { type: "button", className: "lc-icobtn", onClick: () => setWifiOpen(true), title: "WiFi 无线调试(adb pair + connect)" }, "📶"),
965
1039
  h("input", {
966
1040
  ref: fileInputRef,
967
1041
  type: "file",
@@ -969,19 +1043,19 @@ window.__ModuleLoader__.load({
969
1043
  style: { display: "none" },
970
1044
  onChange: (e) => { installApkFile(e.target.files?.[0] ?? null); e.target.value = ""; },
971
1045
  }),
972
- h("button", { type: "button", className: "lc-btn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, installingApk ? "安装中…" : "装APK"),
973
- h("button", { type: "button", className: "lc-btn", "data-on": paused ? "" : undefined, onClick: togglePause },
974
- paused ? "继续(" + pendingCount + ")" : "暂停"),
975
- h("button", { type: "button", className: "lc-btn", onClick: clearLog }, "清空"),
976
- h("button", { type: "button", className: "lc-btn", onClick: copyLog }, "复制"),
977
- h("button", { type: "button", className: "lc-btn", onClick: exportLog }, "导出"),
1046
+ h("button", { type: "button", className: "lc-icobtn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, "📦"),
1047
+ h("button", { type: "button", className: "lc-icobtn", "data-on": paused ? "" : undefined, onClick: togglePause, title: "暂停冻结视图(新日志缓冲) / 继续回放" },
1048
+ paused ? "" : ""),
1049
+ h("button", { type: "button", className: "lc-icobtn", onClick: clearLog, title: "清空当前视图" }, "🗑"),
1050
+ h("button", { type: "button", className: "lc-icobtn", onClick: copyLog, title: "复制过滤后日志" }, "📋"),
1051
+ h("button", { type: "button", className: "lc-icobtn", onClick: exportLog, title: "导出过滤后日志 .txt" }, ""),
978
1052
  h("button", {
979
1053
  type: "button",
980
- className: "lc-btn",
1054
+ className: "lc-icobtn",
981
1055
  "data-on": autoScroll ? "" : undefined,
982
1056
  title: "新日志自动滚到底部",
983
1057
  onClick: () => setAutoScroll(!autoScroll),
984
- }, "自动滚动"),
1058
+ }, "📌"),
985
1059
  )
986
1060
  : null,
987
1061
  tab === "log" || tab === "screen"
@@ -1025,16 +1099,32 @@ window.__ModuleLoader__.load({
1025
1099
  rePids.length === 0
1026
1100
  ? h("div", { className: "lc-re-row", title: "点击「进程」或等待自动加载" }, "(尚未加载 — 点击「进程」刷新)")
1027
1101
  : 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(""); } },
1102
+ 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
1103
  p.pid + " " + p.name)))),
1030
1104
  h("div", { className: "lc-re-col2" },
1105
+ h("div", { className: "lc-re-head" }, "模块 / so 库 (" + reMaps.length + ") — 点击转储基址"),
1106
+ h("div", { className: "lc-re-list" },
1107
+ reMapError !== ""
1108
+ ? h("div", { className: "lc-re-row" }, reMapError)
1109
+ : reMaps.length === 0
1110
+ ? h("div", { className: "lc-re-row" }, "(无模块 — 普通设备无法读取其他应用 maps,需 root)")
1111
+ : reMaps.map((mo) =>
1112
+ h("div", { key: mo.path, className: "lc-re-row", title: mo.path + " [" + mo.perms + "]", onClick: () => reDumpAt(mo.base, 256) },
1113
+ (mo.path.split("/").pop() ?? mo.path) + " @" + mo.base + " " + mo.perms))),
1031
1114
  h("div", { className: "lc-re-head" }, "匹配地址(点击转储 256B)" + (reResults.length > 0 ? " · " + reResults.length : "")),
1032
1115
  h("div", { className: "lc-re-list" },
1033
1116
  reResults.map((o) =>
1034
1117
  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
1118
  "0x" + o.toString(16)))),
1119
+ h("div", { className: "lc-re-head" }, "手动转储"),
1120
+ h("div", { className: "lc-re-man" },
1121
+ h("input", { placeholder: "地址 0x7a0000…", value: reManualAddr, onChange: (e) => setReManualAddr(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") manualDump(); }, style: { flex: 1, minWidth: 120 } }),
1122
+ h("select", { className: "lc-select", value: String(reDumpLen), onChange: (e) => setReDumpLen(Number(e.target.value)) },
1123
+ [128, 256, 1024, 4096].map((n) => h("option", { key: n, value: String(n) }, n + "B"))),
1124
+ h("button", { type: "button", className: "lc-btn", onClick: manualDump }, "转储"),
1125
+ ),
1036
1126
  reDump !== ""
1037
- ? h("div", { className: "lc-re-head" }, "转储 " + reDumpAddr)
1127
+ ? h("div", { className: "lc-re-head" }, "转储 " + reDumpAddr + " · " + reDumpLen + "B")
1038
1128
  : null,
1039
1129
  reDump !== "" ? h("div", { className: "lc-re-dump" }, reDump) : null,
1040
1130
  reError !== "" ? h("div", { className: "lc-re-err" }, reError) : null,
@@ -1048,6 +1138,7 @@ window.__ModuleLoader__.load({
1048
1138
  h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~" + screenFps + "fps)" : "投屏未开启")),
1049
1139
  h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 140 } }),
1050
1140
  h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
1141
+ h("button", { type: "button", className: "lc-btn", onClick: toggleScreenFullscreen, title: "全屏查看(Esc 退出)" }, "⛶"),
1051
1142
  h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
1052
1143
  ),
1053
1144
  !hostScreenCapable
@@ -1060,10 +1151,10 @@ window.__ModuleLoader__.load({
1060
1151
  : eventsOn
1061
1152
  ? (filteredEvents.length === 0
1062
1153
  ? h("div", { className: "lc-empty" }, "events 事件缓冲(已开启流,等待事件…)")
1063
- : h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef }))
1154
+ : h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine }))
1064
1155
  : (filtered.length === 0
1065
1156
  ? h("div", { className: "lc-empty" }, paused ? "已暂停(" + entries.length + " 条已缓冲)" : "暂无日志")
1066
- : h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef })),
1157
+ : h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine })),
1067
1158
  tab === "log" && !eventsOn && live && !paused
1068
1159
  ? h("button", { type: "button", className: "lc-fab", onClick: togglePause, title: "暂停:冻结当前视图(新日志进入缓冲)" }, "⏸ 暂停")
1069
1160
  : null,
@@ -1172,8 +1263,32 @@ window.__ModuleLoader__.load({
1172
1263
  );
1173
1264
  }
1174
1265
 
1266
+ /** Split `text` on keyword hits; render hit ranges as <mark>. */
1267
+ function highlightMsg(text, keyword) {
1268
+ const terms = (keyword ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
1269
+ if (terms.length === 0 || text === "") return text;
1270
+ const lower = text.toLowerCase();
1271
+ const ranges = [];
1272
+ for (const t of terms) {
1273
+ const idx = lower.indexOf(t);
1274
+ if (idx >= 0) ranges.push([idx, idx + t.length]);
1275
+ }
1276
+ if (ranges.length === 0) return text;
1277
+ ranges.sort((a, b) => a[0] - b[0]);
1278
+ const parts = [];
1279
+ let pos = 0;
1280
+ for (const [s, e] of ranges) {
1281
+ if (s < pos) continue; // overlapping hits: keep the first
1282
+ if (s > pos) parts.push(text.slice(pos, s));
1283
+ parts.push(h("mark", { className: "lc-hit", key: s }, text.slice(s, e)));
1284
+ pos = e;
1285
+ }
1286
+ if (pos < text.length) parts.push(text.slice(pos));
1287
+ return parts;
1288
+ }
1289
+
1175
1290
  /** Windowed log list: fixed 20px rows, only the visible slice is rendered. */
1176
- function VirtualLog({ entries, scrollTop, onScrollTop, bodyRef }) {
1291
+ function VirtualLog({ entries, scrollTop, onScrollTop, bodyRef, keyword, onTagClick, onLineDblClick }) {
1177
1292
  const ROW = 20;
1178
1293
  const height = useRef(0);
1179
1294
  const [viewport, setViewport] = useState({ top: 0, bottom: 100 });
@@ -1212,13 +1327,14 @@ window.__ModuleLoader__.load({
1212
1327
  key: i,
1213
1328
  className: "lc-line" + (e.cont === true ? " cont" : "") + (isCrash(e) ? " crash" : ""),
1214
1329
  style: { top: i * ROW },
1215
- title: e.raw,
1330
+ title: e.raw + "\n(点 tag 过滤 · 双击复制该行)",
1331
+ onDoubleClick: () => { if (onLineDblClick) onLineDblClick(e.raw); },
1216
1332
  },
1217
1333
  e.ts !== "" ? h("span", { className: "ts" }, e.ts) : null,
1218
1334
  e.pid > 0 ? h("span", { className: "pid" }, e.pid + "-" + e.tid) : null,
1219
1335
  e.level !== "" ? h("span", { className: "lv " + level }, level) : null,
1220
- e.tag !== "" ? h("span", { className: "tag" }, e.tag) : null,
1221
- h("span", { className: "msg" }, e.msg !== "" ? e.msg : e.raw),
1336
+ e.tag !== "" ? h("span", { className: "tag", title: "按此 tag 过滤", onClick: (ev) => { ev.stopPropagation(); if (onTagClick) onTagClick(e.tag); } }, e.tag) : null,
1337
+ h("span", { className: "msg" }, highlightMsg(e.msg !== "" ? e.msg : e.raw, keyword)),
1222
1338
  ),
1223
1339
  );
1224
1340
  }
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.4",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {