@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.
- package/lib/client.js +145 -29
- package/lib/index.js +45 -15
- 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
|
-
|
|
731
|
-
|
|
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=
|
|
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: () =>
|
|
919
|
-
h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () =>
|
|
920
|
-
h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () =>
|
|
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-
|
|
961
|
-
h("button", { type: "button", className: "lc-
|
|
962
|
-
h("button", { type: "button", className: "lc-
|
|
963
|
-
h("button", { type: "button", className: "lc-
|
|
964
|
-
h("button", { type: "button", className: "lc-
|
|
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-
|
|
973
|
-
h("button", { type: "button", className: "lc-
|
|
974
|
-
paused ? "
|
|
975
|
-
h("button", { type: "button", className: "lc-
|
|
976
|
-
h("button", { type: "button", className: "lc-
|
|
977
|
-
h("button", { type: "button", className: "lc-
|
|
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-
|
|
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
|
-
|
|
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.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|