@windypro-rourou/dsh-logcat 0.6.0-preview.4 → 0.6.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/lib/client.js +98 -29
- package/lib/index.js +17 -6
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -136,9 +136,13 @@ window.__ModuleLoader__.load({
|
|
|
136
136
|
.lc-screen { position: absolute; inset: 0; display: flex; flex-direction: column; }
|
|
137
137
|
.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
138
|
.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; }
|
|
139
|
-
.lc-screen-img { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: crosshair; background: light-dark(rgba(0,0,0,.03), rgba(255,255,255,.03)); }
|
|
140
|
-
.lc-screen-img img { max-width: 100%; max-height: 100%; object-fit: contain; user-select: none; }
|
|
139
|
+
.lc-screen-img { flex: 1; min-height: 0; overflow: auto; display: flex; align-items: center; justify-content: center; cursor: crosshair; background: light-dark(rgba(0,0,0,.03), rgba(255,255,255,.03)); position: relative; }
|
|
140
|
+
.lc-screen-img img, .lc-screen-img canvas { max-width: 100%; max-height: 100%; object-fit: contain; user-select: none; }
|
|
141
141
|
.lc-screen-hint { color: light-dark(#6b7078, #9aa0a8); }
|
|
142
|
+
.lc-pausedbar { display: flex; align-items: center; gap: 8px; padding: 5px 12px; background: light-dark(rgba(251,192,45,.16), rgba(251,192,45,.18)); border-bottom: 1px solid rgba(251,192,45,.45); flex: none; font-size: 12px; color: #fbc02d; }
|
|
143
|
+
.lc-pausedbar .lc-btn { padding: 2px 10px; }
|
|
144
|
+
.lc-fab { position: absolute; right: 14px; bottom: 14px; z-index: 4; border: 1px solid rgba(66,133,244,.45); background: light-dark(rgba(66,133,244,.14), rgba(66,133,244,.24)); color: #4285f4; font: inherit; font-size: 12px; font-weight: 700; padding: 8px 14px; border-radius: 999px; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,.22); }
|
|
145
|
+
.lc-fab:hover { background: light-dark(rgba(66,133,244,.22), rgba(66,133,244,.34)); }
|
|
142
146
|
`;
|
|
143
147
|
//#endregion
|
|
144
148
|
|
|
@@ -183,6 +187,7 @@ window.__ModuleLoader__.load({
|
|
|
183
187
|
const [entries, setEntries] = useState([]);
|
|
184
188
|
const [connected, setConnected] = useState(false);
|
|
185
189
|
const [paused, setPaused] = useState(false);
|
|
190
|
+
const [pendingCount, setPendingCount] = useState(0);
|
|
186
191
|
const [currentPackage, setCurrentPackage] = useState("");
|
|
187
192
|
const [pkgInput, setPkgInput] = useState("");
|
|
188
193
|
const [installingAdb, setInstallingAdb] = useState(false);
|
|
@@ -225,6 +230,8 @@ window.__ModuleLoader__.load({
|
|
|
225
230
|
const [screenImg, setScreenImg] = useState(null);
|
|
226
231
|
const [screenText, setScreenText] = useState("");
|
|
227
232
|
const [screenOn, setScreenOn] = useState(false);
|
|
233
|
+
const [screenFps, setScreenFps] = useState(0);
|
|
234
|
+
const [screenHasFrame, setScreenHasFrame] = useState(false);
|
|
228
235
|
const [hostScreenCapable, setHostScreenCapable] = useState(false);
|
|
229
236
|
const panelOpen = useSyncExternalStore(controller.subscribe, controller.getSnapshot).panelOpen;
|
|
230
237
|
|
|
@@ -241,10 +248,14 @@ window.__ModuleLoader__.load({
|
|
|
241
248
|
const fileInputRef = useRef(null);
|
|
242
249
|
const screenWrapRef = useRef(null);
|
|
243
250
|
const screenImgRef = useRef(null);
|
|
251
|
+
const screenCanvasRef = useRef(null);
|
|
244
252
|
const screenDragRef = useRef(null);
|
|
245
253
|
const screenWantRef = useRef(false);
|
|
246
254
|
const handleBinaryRef = useRef(null);
|
|
247
255
|
const prevSerialRef = useRef(serial);
|
|
256
|
+
const screenDecodingRef = useRef(false);
|
|
257
|
+
const screenPendingRef = useRef(null);
|
|
258
|
+
const screenFpsTsRef = useRef([]);
|
|
248
259
|
|
|
249
260
|
useEffect(() => { serialRef.current = serial; }, [serial]);
|
|
250
261
|
useEffect(() => { pausedRef.current = paused; }, [paused]);
|
|
@@ -293,10 +304,19 @@ window.__ModuleLoader__.load({
|
|
|
293
304
|
|
|
294
305
|
const appendEntries = (incoming) => {
|
|
295
306
|
const next = entriesRef.current.concat(incoming);
|
|
296
|
-
|
|
307
|
+
let removed = 0;
|
|
308
|
+
if (next.length > 5000) { removed = next.length - 5000; next.splice(0, removed); }
|
|
297
309
|
entriesRef.current = next;
|
|
298
310
|
setEntries(next);
|
|
299
|
-
if (autoScrollRef.current)
|
|
311
|
+
if (autoScrollRef.current) {
|
|
312
|
+
requestAnimationFrame(() => scrollToBottom());
|
|
313
|
+
} else if (removed > 0) {
|
|
314
|
+
// Anchor the viewport: trimming old rows shifts array indexes, so
|
|
315
|
+
// compensate the scroll position to keep what the user is reading
|
|
316
|
+
// visually in place (no jumping while auto-scroll is off).
|
|
317
|
+
const body = bodyRef.current;
|
|
318
|
+
if (body !== null) body.scrollTop = Math.max(0, body.scrollTop - removed * 20);
|
|
319
|
+
}
|
|
300
320
|
};
|
|
301
321
|
|
|
302
322
|
const applyFrame = (frame) => {
|
|
@@ -328,6 +348,7 @@ window.__ModuleLoader__.load({
|
|
|
328
348
|
if (pausedRef.current) {
|
|
329
349
|
pendingRef.current.push(frame);
|
|
330
350
|
if (pendingRef.current.length > 800) pendingRef.current.splice(0, pendingRef.current.length - 800);
|
|
351
|
+
setPendingCount(pendingRef.current.length);
|
|
331
352
|
return;
|
|
332
353
|
}
|
|
333
354
|
if (frame.serial !== serialRef.current) return;
|
|
@@ -489,10 +510,13 @@ window.__ModuleLoader__.load({
|
|
|
489
510
|
const togglePause = () => {
|
|
490
511
|
const next = !paused;
|
|
491
512
|
setPaused(next);
|
|
492
|
-
if (!next
|
|
493
|
-
|
|
494
|
-
pendingRef.current
|
|
495
|
-
|
|
513
|
+
if (!next) {
|
|
514
|
+
setPendingCount(0);
|
|
515
|
+
if (pendingRef.current.length > 0) {
|
|
516
|
+
const replay = pendingRef.current.filter((f) => f.serial === serialRef.current);
|
|
517
|
+
pendingRef.current = [];
|
|
518
|
+
if (replay.length > 0) appendEntries(replay.map((f) => f.entry));
|
|
519
|
+
}
|
|
496
520
|
}
|
|
497
521
|
};
|
|
498
522
|
|
|
@@ -671,6 +695,11 @@ window.__ModuleLoader__.load({
|
|
|
671
695
|
.catch(() => setWifiOut("网络错误"));
|
|
672
696
|
};
|
|
673
697
|
|
|
698
|
+
// RE workbench: auto-load the process list when the tab opens.
|
|
699
|
+
useEffect(() => {
|
|
700
|
+
if (tab === "re" && serial !== "") loadProcesses();
|
|
701
|
+
}, [tab, serial]);
|
|
702
|
+
|
|
674
703
|
const loadProcesses = () => {
|
|
675
704
|
fetch(API_BASE + "/processes?serial=" + encodeURIComponent(serial) + "&filter=" + encodeURIComponent(reFilter))
|
|
676
705
|
.then((res) => res.json())
|
|
@@ -728,6 +757,32 @@ window.__ModuleLoader__.load({
|
|
|
728
757
|
}, [tab, serial, panelOpen]);
|
|
729
758
|
|
|
730
759
|
// Binary WS frames: 4-byte JSON header length + header + PNG payload.
|
|
760
|
+
// Decoding goes through createImageBitmap into a canvas (no base64
|
|
761
|
+
// data-URL churn per frame); only the newest pending frame is kept.
|
|
762
|
+
const pumpScreenFrame = async () => {
|
|
763
|
+
if (screenDecodingRef.current) return;
|
|
764
|
+
screenDecodingRef.current = true;
|
|
765
|
+
try {
|
|
766
|
+
while (screenPendingRef.current !== null) {
|
|
767
|
+
const bytes = screenPendingRef.current;
|
|
768
|
+
screenPendingRef.current = null;
|
|
769
|
+
const canvas = screenCanvasRef.current;
|
|
770
|
+
if (canvas === null) break; // wait until the canvas mounts
|
|
771
|
+
try {
|
|
772
|
+
const bmp = await createImageBitmap(new Blob([bytes], { type: "image/png" }));
|
|
773
|
+
if (canvas.width !== bmp.width) canvas.width = bmp.width;
|
|
774
|
+
if (canvas.height !== bmp.height) canvas.height = bmp.height;
|
|
775
|
+
const c2d = canvas.getContext("2d");
|
|
776
|
+
c2d.drawImage(bmp, 0, 0);
|
|
777
|
+
bmp.close();
|
|
778
|
+
setScreenHasFrame(true);
|
|
779
|
+
} catch { /* undecodable frame — skip */ }
|
|
780
|
+
}
|
|
781
|
+
} finally {
|
|
782
|
+
screenDecodingRef.current = false;
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
731
786
|
const handleBinary = (buffer) => {
|
|
732
787
|
try {
|
|
733
788
|
const view = new DataView(buffer);
|
|
@@ -735,12 +790,14 @@ window.__ModuleLoader__.load({
|
|
|
735
790
|
const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 4, hlen)));
|
|
736
791
|
if (header.type !== "simage" || header.serial !== serialRef.current) return;
|
|
737
792
|
const bytes = new Uint8Array(buffer, 4 + hlen);
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
793
|
+
// Sliding 1.5s window → rough fps readout.
|
|
794
|
+
const now = performance.now();
|
|
795
|
+
const ts = screenFpsTsRef.current;
|
|
796
|
+
ts.push(now);
|
|
797
|
+
while (ts.length > 0 && now - ts[0] > 1500) ts.shift();
|
|
798
|
+
setScreenFps(Math.min(ts.length, 10));
|
|
799
|
+
screenPendingRef.current = bytes;
|
|
800
|
+
void pumpScreenFrame();
|
|
744
801
|
} catch { /* malformed frame */ }
|
|
745
802
|
};
|
|
746
803
|
handleBinaryRef.current = handleBinary;
|
|
@@ -793,13 +850,16 @@ window.__ModuleLoader__.load({
|
|
|
793
850
|
};
|
|
794
851
|
|
|
795
852
|
const downloadScreen = () => {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
853
|
+
const canvas = screenCanvasRef.current;
|
|
854
|
+
if (canvas === null || canvas.width <= 1) return;
|
|
855
|
+
try {
|
|
856
|
+
const a = document.createElement("a");
|
|
857
|
+
a.href = canvas.toDataURL("image/png");
|
|
858
|
+
a.download = "screen-" + (serial || "device") + "-" + new Date().toISOString().replace(/[:.]/g, "-") + ".png";
|
|
859
|
+
document.body.appendChild(a);
|
|
860
|
+
a.click();
|
|
861
|
+
a.remove();
|
|
862
|
+
} catch { /* empty canvas */ }
|
|
803
863
|
};
|
|
804
864
|
|
|
805
865
|
return h("div", { className: "lc-panel" },
|
|
@@ -911,7 +971,7 @@ window.__ModuleLoader__.load({
|
|
|
911
971
|
}),
|
|
912
972
|
h("button", { type: "button", className: "lc-btn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, installingApk ? "安装中…" : "装APK"),
|
|
913
973
|
h("button", { type: "button", className: "lc-btn", "data-on": paused ? "" : undefined, onClick: togglePause },
|
|
914
|
-
paused ? "继续" : "暂停"),
|
|
974
|
+
paused ? "继续(" + pendingCount + ")" : "暂停"),
|
|
915
975
|
h("button", { type: "button", className: "lc-btn", onClick: clearLog }, "清空"),
|
|
916
976
|
h("button", { type: "button", className: "lc-btn", onClick: copyLog }, "复制"),
|
|
917
977
|
h("button", { type: "button", className: "lc-btn", onClick: exportLog }, "导出"),
|
|
@@ -944,10 +1004,15 @@ window.__ModuleLoader__.load({
|
|
|
944
1004
|
h("button", { type: "button", className: "lc-btn", onClick: () => setHistoryNote("") }, "关闭"),
|
|
945
1005
|
)
|
|
946
1006
|
: null,
|
|
1007
|
+
paused && tab === "log" && !eventsOn
|
|
1008
|
+
? h("div", { className: "lc-pausedbar" },
|
|
1009
|
+
h("span", { style: { flex: 1 } }, "⏸ 已暂停 — 新日志缓冲中(" + pendingCount + " 条),列表已冻结"),
|
|
1010
|
+
h("button", { type: "button", className: "lc-btn", onClick: togglePause, title: "恢复实时,回放缓冲日志" }, "继续"),
|
|
1011
|
+
)
|
|
1012
|
+
: null,
|
|
947
1013
|
h("div", { className: "lc-body", ref: bodyRef },
|
|
948
1014
|
tab === "re"
|
|
949
|
-
? h("div", { className: "lc-re" },
|
|
950
|
-
h("div", { className: "lc-re-top" },
|
|
1015
|
+
? h("div", { className: "lc-re" }, h("div", { className: "lc-re-top" },
|
|
951
1016
|
h("input", { placeholder: "进程过滤…", value: reFilter, onChange: (e) => setReFilter(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") loadProcesses(); }, style: { width: 120 } }),
|
|
952
1017
|
h("button", { type: "button", className: "lc-btn", onClick: loadProcesses }, "进程"),
|
|
953
1018
|
h("input", { placeholder: "搜索: hello 或 hex:48656c6c6f", value: rePattern, onChange: (e) => setRePattern(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") reSearch(); }, style: { flex: 1, minWidth: 140 } }),
|
|
@@ -957,7 +1022,9 @@ window.__ModuleLoader__.load({
|
|
|
957
1022
|
h("div", { className: "lc-re-col" },
|
|
958
1023
|
h("div", { className: "lc-re-head" }, "进程 (" + rePids.length + ")"),
|
|
959
1024
|
h("div", { className: "lc-re-list" },
|
|
960
|
-
rePids.
|
|
1025
|
+
rePids.length === 0
|
|
1026
|
+
? h("div", { className: "lc-re-row", title: "点击「进程」或等待自动加载" }, "(尚未加载 — 点击「进程」刷新)")
|
|
1027
|
+
: rePids.map((p) =>
|
|
961
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(""); } },
|
|
962
1029
|
p.pid + " " + p.name)))),
|
|
963
1030
|
h("div", { className: "lc-re-col2" },
|
|
@@ -978,7 +1045,7 @@ window.__ModuleLoader__.load({
|
|
|
978
1045
|
? h("div", { className: "lc-screen" },
|
|
979
1046
|
h("div", { className: "lc-screen-bar" },
|
|
980
1047
|
h("span", { className: "lc-dot " + (screenOn && hostScreenCapable ? "on" : "off") }),
|
|
981
|
-
h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~
|
|
1048
|
+
h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~" + screenFps + "fps)" : "投屏未开启")),
|
|
982
1049
|
h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 140 } }),
|
|
983
1050
|
h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
|
|
984
1051
|
h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
|
|
@@ -986,9 +1053,8 @@ window.__ModuleLoader__.load({
|
|
|
986
1053
|
!hostScreenCapable
|
|
987
1054
|
? h("div", { className: "lc-empty" }, "⚠ 运行中的 GUI 宿主版本过旧(0.6.0-preview.1)——请重启 GUI(dsh web)后刷新,投屏功能才会生效")
|
|
988
1055
|
: h("div", { className: "lc-screen-img", ref: screenWrapRef, onMouseDown: screenMouseDown, onMouseUp: screenMouseUp, onMouseLeave: screenMouseUp },
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
: h("div", { className: "lc-empty" }, "等待画面…(设备连接后自动开始,1fps)"),
|
|
1056
|
+
h("canvas", { ref: screenCanvasRef, alt: "设备屏幕", style: { width: "1080px", height: "2408px" } }),
|
|
1057
|
+
!screenHasFrame ? h("div", { className: "lc-empty" }, "等待画面…(设备连接后自动开始)") : null,
|
|
992
1058
|
),
|
|
993
1059
|
)
|
|
994
1060
|
: eventsOn
|
|
@@ -998,6 +1064,9 @@ window.__ModuleLoader__.load({
|
|
|
998
1064
|
: (filtered.length === 0
|
|
999
1065
|
? h("div", { className: "lc-empty" }, paused ? "已暂停(" + entries.length + " 条已缓冲)" : "暂无日志")
|
|
1000
1066
|
: h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef })),
|
|
1067
|
+
tab === "log" && !eventsOn && live && !paused
|
|
1068
|
+
? h("button", { type: "button", className: "lc-fab", onClick: togglePause, title: "暂停:冻结当前视图(新日志进入缓冲)" }, "⏸ 暂停")
|
|
1069
|
+
: null,
|
|
1001
1070
|
),
|
|
1002
1071
|
crashModal
|
|
1003
1072
|
? h("div", { className: "lc-overlay", onClick: () => setCrashModal(false) },
|
package/lib/index.js
CHANGED
|
@@ -687,19 +687,21 @@ export class AdbEngine {
|
|
|
687
687
|
return picked.slice(-lines)
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
-
/** Start / stop the
|
|
690
|
+
/** Start / stop the screen-stream loop for a device (panel live view). */
|
|
691
691
|
setScreen(serial, on) {
|
|
692
692
|
const existing = this.screenLoops.get(serial)
|
|
693
693
|
if (on) {
|
|
694
694
|
if (existing === undefined && this.adb !== null && this.devices.get(serial)?.state === 'device') {
|
|
695
|
-
const loop = { timer: null, busy: false }
|
|
695
|
+
const loop = { timer: null, busy: false, lastHash: null }
|
|
696
696
|
this.screenLoops.set(serial, loop)
|
|
697
697
|
const tick = () => {
|
|
698
698
|
if (loop.busy || this.screenLoops.get(serial) !== loop) return
|
|
699
699
|
loop.busy = true
|
|
700
|
-
void this.captureScreenFrame(serial).finally(() => { loop.busy = false })
|
|
700
|
+
void this.captureScreenFrame(serial, loop).finally(() => { loop.busy = false })
|
|
701
701
|
}
|
|
702
|
-
|
|
702
|
+
// 500ms cadence: screencap itself takes 300-700ms, so the busy flag
|
|
703
|
+
// throttles naturally; static screens are deduped by content hash.
|
|
704
|
+
loop.timer = setInterval(tick, 500)
|
|
703
705
|
tick()
|
|
704
706
|
}
|
|
705
707
|
} else if (existing !== undefined) {
|
|
@@ -708,11 +710,20 @@ export class AdbEngine {
|
|
|
708
710
|
}
|
|
709
711
|
}
|
|
710
712
|
|
|
711
|
-
/** Capture one frame and broadcast it as a binary WS frame. */
|
|
712
|
-
async captureScreenFrame(serial) {
|
|
713
|
+
/** Capture one frame and broadcast it as a binary WS frame (deduped when the picture is unchanged). */
|
|
714
|
+
async captureScreenFrame(serial, loop) {
|
|
713
715
|
try {
|
|
714
716
|
const png = await runAdbBinary(this.adb, ['-s', serial, 'exec-out', 'screencap', '-p'], 8000)
|
|
715
717
|
if (png.length < 100) return
|
|
718
|
+
// Cheap content hash (FNV-1a over the full buffer) — skip broadcasts of
|
|
719
|
+
// identical frames so idle screens cost nothing on the client.
|
|
720
|
+
let hash = 2166136261
|
|
721
|
+
for (let i = 0; i < png.length; i++) {
|
|
722
|
+
hash ^= png[i]
|
|
723
|
+
hash = Math.imul(hash, 16777619)
|
|
724
|
+
}
|
|
725
|
+
if (loop !== undefined && hash === loop.lastHash) return
|
|
726
|
+
if (loop !== undefined) loop.lastHash = hash
|
|
716
727
|
this.broadcastBinary({ type: 'simage', serial, len: png.length, ts: Date.now() }, png)
|
|
717
728
|
} catch { /* frame dropped */ }
|
|
718
729
|
}
|
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.0
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|