@windypro-rourou/dsh-logcat 0.5.0-preview.1 → 0.5.0-preview.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 +99 -25
- package/lib/index.js +50 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -33,51 +33,64 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
[data-dsh-logcat-entry][data-active] { background: rgba(128,128,128,.22); }
|
|
34
34
|
[data-dsh-logcat-entry] .lc-entry-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; flex: none; }
|
|
35
35
|
[data-dsh-logcat-entry] .lc-entry-label { font-size: 13px; line-height: 1.2; opacity: .92; }
|
|
36
|
-
.dsh-logcat-view { position: fixed; top: 0; right: 0; bottom: 0; width: min(620px, 94vw);
|
|
36
|
+
.dsh-logcat-view { position: fixed; top: 0; right: 0; bottom: 0; width: min(620px, 94vw); z-index: 9999; }
|
|
37
37
|
.dsh-logcat-view[hidden] { display: none !important; }
|
|
38
|
+
.dsh-logcat-panel {
|
|
39
|
+
position: absolute; inset: 0; display: flex; flex-direction: column;
|
|
40
|
+
background: light-dark(#ffffff, #171a1f);
|
|
41
|
+
color: light-dark(#1a1d23, #dfe3ea);
|
|
42
|
+
border-left: 1px solid light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14));
|
|
43
|
+
box-shadow: -10px 0 28px rgba(0,0,0,.22);
|
|
44
|
+
}
|
|
38
45
|
.dsh-logcat-resize { position: absolute; left: -6px; top: 0; bottom: 0; width: 12px; cursor: col-resize; z-index: 2; display: flex; align-items: center; justify-content: center; }
|
|
39
|
-
.dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: rgba(
|
|
40
|
-
.dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: rgba(
|
|
41
|
-
.dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: rgba(
|
|
46
|
+
.dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: light-dark(rgba(0,0,0,.3), rgba(255,255,255,.35)); transition: background .15s, height .15s; }
|
|
47
|
+
.dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: light-dark(rgba(0,0,0,.6), rgba(255,255,255,.75)); height: 72px; }
|
|
48
|
+
.dsh-logcat-resize:hover, .dsh-logcat-resize[data-drag] { background: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.14)); }
|
|
42
49
|
.lc-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
|
43
|
-
.lc-header { display: flex; align-items: center; gap: 8px; padding:
|
|
50
|
+
.lc-header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; }
|
|
44
51
|
.lc-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; display: flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 6px; font-size: 13px; }
|
|
45
|
-
.lc-back:hover { background: rgba(
|
|
46
|
-
.lc-title { font-size: 15px; font-weight:
|
|
52
|
+
.lc-back:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
|
|
53
|
+
.lc-title { font-size: 15px; font-weight: 700; margin: 0; letter-spacing: .2px; }
|
|
47
54
|
.lc-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
48
|
-
.lc-dot.on { background: #4caf50; }
|
|
55
|
+
.lc-dot.on { background: #4caf50; box-shadow: 0 0 6px rgba(76,175,80,.6); }
|
|
49
56
|
.lc-dot.off { background: #9e9e9e; }
|
|
50
57
|
.lc-dot.warn { background: #ff9800; }
|
|
51
|
-
.lc-
|
|
52
|
-
.lc-
|
|
53
|
-
.lc-
|
|
58
|
+
.lc-stats { display: flex; align-items: center; gap: 6px; padding: 6px 14px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); flex: none; flex-wrap: wrap; font-size: 11px; }
|
|
59
|
+
.lc-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 999px; background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); color: light-dark(#4a4f58, #b9bec7); white-space: nowrap; }
|
|
60
|
+
.lc-chip b { color: inherit; font-weight: 600; }
|
|
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
|
+
.lc-mem-fill { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, #4caf50, #fbc02d); }
|
|
63
|
+
.lc-mem-fill.hot { background: linear-gradient(90deg, #fbc02d, #ef5350); }
|
|
64
|
+
.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
|
+
.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
|
+
.lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
|
|
54
67
|
.lc-search { flex: 1; min-width: 120px; max-width: 320px; }
|
|
55
|
-
.lc-levels { display: flex; gap: 2px; }
|
|
56
|
-
.lc-level { border:
|
|
57
|
-
.lc-level:hover { background: rgba(
|
|
58
|
-
.lc-level[data-on] { background:
|
|
68
|
+
.lc-levels { display: flex; gap: 2px; background: light-dark(rgba(0,0,0,.04), rgba(255,255,255,.06)); padding: 2px; border-radius: 7px; }
|
|
69
|
+
.lc-level { border: 0; background: transparent; color: inherit; font: inherit; font-size: 12px; font-weight: 600; width: 24px; height: 22px; border-radius: 5px; cursor: pointer; }
|
|
70
|
+
.lc-level:hover { background: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.1)); }
|
|
71
|
+
.lc-level[data-on] { background: light-dark(#fff, #2a2f37); box-shadow: 0 1px 3px rgba(0,0,0,.2); }
|
|
59
72
|
.lc-level.v { color: #9e9e9e; } .lc-level.d { color: #4fc3f7; } .lc-level.i { color: #4caf50; }
|
|
60
73
|
.lc-level.w { color: #fbc02d; } .lc-level.e { color: #ef5350; } .lc-level.f { color: #ab47bc; }
|
|
61
|
-
.lc-btn { border: 1px solid rgba(
|
|
62
|
-
.lc-btn:hover { background: rgba(
|
|
63
|
-
.lc-btn[data-on] { background: rgba(
|
|
74
|
+
.lc-btn { border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 5px 10px; border-radius: 7px; cursor: pointer; transition: background .12s; }
|
|
75
|
+
.lc-btn:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
|
|
76
|
+
.lc-btn[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); border-color: rgba(66,133,244,.45); color: #4285f4; }
|
|
64
77
|
.lc-body { flex: 1; min-height: 0; position: relative; overflow: hidden; }
|
|
65
78
|
.lc-log { position: absolute; inset: 0; overflow: auto; font-family: Consolas, "Cascadia Mono", "Courier New", monospace; font-size: 12px; line-height: 20px; }
|
|
66
79
|
.lc-log-inner { position: relative; }
|
|
67
80
|
.lc-line { position: absolute; left: 0; right: 0; padding: 0 12px; white-space: pre; overflow: hidden; text-overflow: ellipsis; cursor: default; }
|
|
68
|
-
.lc-line:hover { background: rgba(
|
|
69
|
-
.lc-line .ts { color:
|
|
70
|
-
.lc-line .pid { color:
|
|
81
|
+
.lc-line:hover { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.07)); }
|
|
82
|
+
.lc-line .ts { color: light-dark(#9aa0a8, #7c838d); margin-right: 8px; }
|
|
83
|
+
.lc-line .pid { color: light-dark(#9aa0a8, #7c838d); margin-right: 6px; }
|
|
71
84
|
.lc-line .lv { display: inline-block; width: 14px; text-align: center; font-weight: 700; margin-right: 6px; }
|
|
72
85
|
.lc-line .lv.V { color: #9e9e9e; } .lc-line .lv.D { color: #4fc3f7; } .lc-line .lv.I { color: #4caf50; }
|
|
73
86
|
.lc-line .lv.W { color: #fbc02d; } .lc-line .lv.E { color: #ef5350; } .lc-line .lv.F { color: #ab47bc; }
|
|
74
87
|
.lc-line .tag { color: #29b6f6; margin-right: 8px; }
|
|
75
|
-
.lc-line.cont .msg { padding-left: 44px; color:
|
|
88
|
+
.lc-line.cont .msg { padding-left: 44px; color: light-dark(#9aa0a8, #7c838d); }
|
|
76
89
|
.lc-line.crash { background: rgba(239,83,80,.14); box-shadow: inset 2px 0 0 #ef5350; }
|
|
77
90
|
.lc-line.crash .tag { color: #ef5350; font-weight: 700; }
|
|
78
91
|
.lc-line.crash .msg { color: #ef5350; }
|
|
79
|
-
.lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color:
|
|
80
|
-
.lc-status { display: flex; align-items: center; gap: 14px; padding:
|
|
92
|
+
.lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: light-dark(#9aa0a8, #7c838d); font-size: 13px; }
|
|
93
|
+
.lc-status { display: flex; align-items: center; gap: 14px; padding: 5px 14px; border-top: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; font-size: 11px; color: light-dark(#6b7078, #9aa0a8); }
|
|
81
94
|
.lc-status b { font-weight: 600; color: inherit; }
|
|
82
95
|
`;
|
|
83
96
|
//#endregion
|
|
@@ -121,6 +134,7 @@ window.__ModuleLoader__.load({
|
|
|
121
134
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
|
122
135
|
const [updateKind, setUpdateKind] = useState("");
|
|
123
136
|
const [updateHint, setUpdateHint] = useState("");
|
|
137
|
+
const [stats, setStats] = useState(null);
|
|
124
138
|
const [level, setLevel] = useState("");
|
|
125
139
|
const [keyword, setKeyword] = useState("");
|
|
126
140
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
@@ -270,6 +284,34 @@ window.__ModuleLoader__.load({
|
|
|
270
284
|
.catch(() => { /* host not up yet */ });
|
|
271
285
|
}, []);
|
|
272
286
|
|
|
287
|
+
// Live device stats (model / memory / cpu / battery) — refresh every 5s.
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
let alive = true;
|
|
290
|
+
let timer = null;
|
|
291
|
+
const load = () => {
|
|
292
|
+
const target = serialRef.current;
|
|
293
|
+
if (target === "") { setStats(null); return; }
|
|
294
|
+
fetch(API_BASE + "/stats?serial=" + encodeURIComponent(target))
|
|
295
|
+
.then((res) => res.json())
|
|
296
|
+
.then((body) => { if (alive) setStats(body?.ready === true ? body : null); })
|
|
297
|
+
.catch(() => { /* device offline */ });
|
|
298
|
+
};
|
|
299
|
+
load();
|
|
300
|
+
timer = setInterval(load, 5000);
|
|
301
|
+
return () => { alive = false; if (timer !== null) clearInterval(timer); };
|
|
302
|
+
}, [serial]);
|
|
303
|
+
|
|
304
|
+
const parseCpu = (line) => {
|
|
305
|
+
if (!line) return undefined;
|
|
306
|
+
const m = /([\d.]+)%cpu\s+.*?\s+([\d.]+)%idle/.exec(line);
|
|
307
|
+
if (m === null) return undefined;
|
|
308
|
+
const total = Number(m[1]);
|
|
309
|
+
if (total <= 0) return undefined;
|
|
310
|
+
return Math.round(((total - Number(m[2])) / total) * 100);
|
|
311
|
+
};
|
|
312
|
+
const BATTERY_STATUS = { 1: "未知", 2: "充电中", 3: "放电", 4: "未充电", 5: "已满" };
|
|
313
|
+
const mb = (kb) => kb !== undefined ? (kb / 1024 / 1024).toFixed(1) : undefined;
|
|
314
|
+
|
|
273
315
|
const filtered = useMemo(() => {
|
|
274
316
|
const needle = keyword.trim().toLowerCase();
|
|
275
317
|
return entries.filter((e) => {
|
|
@@ -391,6 +433,33 @@ window.__ModuleLoader__.load({
|
|
|
391
433
|
h("option", { key: d.serial, value: d.serial },
|
|
392
434
|
(d.model !== "" ? d.model + " · " : "") + d.serial + " [" + d.state + "]"))),
|
|
393
435
|
),
|
|
436
|
+
h("div", { className: "lc-stats" },
|
|
437
|
+
stats?.model
|
|
438
|
+
? h("span", { className: "lc-chip", title: "设备型号 / 系统版本" },
|
|
439
|
+
stats.model + (stats.androidVersion ? " · Android " + stats.androidVersion : ""))
|
|
440
|
+
: null,
|
|
441
|
+
stats?.resolution
|
|
442
|
+
? h("span", { className: "lc-chip", title: "屏幕分辨率" }, stats.resolution)
|
|
443
|
+
: null,
|
|
444
|
+
stats?.memPct !== undefined
|
|
445
|
+
? h("span", { className: "lc-chip", title: "内存 " + (mb(stats.usedMemKb) ?? "?") + "G / " + (mb(stats.totalMemKb) ?? "?") + "G" },
|
|
446
|
+
"内存 ",
|
|
447
|
+
h("span", { className: "lc-mem-bar" },
|
|
448
|
+
h("span", {
|
|
449
|
+
className: "lc-mem-fill" + (stats.memPct > 85 ? " hot" : ""),
|
|
450
|
+
style: { width: stats.memPct + "%" },
|
|
451
|
+
})),
|
|
452
|
+
" " + stats.memPct + "%")
|
|
453
|
+
: null,
|
|
454
|
+
parseCpu(stats?.cpu) !== undefined
|
|
455
|
+
? h("span", { className: "lc-chip", title: "CPU 使用率(采样)" }, "CPU " + parseCpu(stats.cpu) + "%")
|
|
456
|
+
: null,
|
|
457
|
+
stats?.batteryLevel !== undefined
|
|
458
|
+
? h("span", { className: "lc-chip", title: "电量" },
|
|
459
|
+
"电量 " + stats.batteryLevel + "%" + (BATTERY_STATUS[stats.batteryStatus] !== undefined ? " · " + BATTERY_STATUS[stats.batteryStatus] : ""))
|
|
460
|
+
: null,
|
|
461
|
+
stats === null && serial !== "" ? h("span", { className: "lc-chip" }, "正在采集设备状态…") : null,
|
|
462
|
+
),
|
|
394
463
|
h("div", { className: "lc-toolbar" },
|
|
395
464
|
h("div", { className: "lc-levels", role: "group", "aria-label": "日志级别" },
|
|
396
465
|
h("button", {
|
|
@@ -689,6 +758,8 @@ window.__ModuleLoader__.load({
|
|
|
689
758
|
root = undefined;
|
|
690
759
|
container?.remove();
|
|
691
760
|
handle = undefined;
|
|
761
|
+
// Wrapper keeps the resize handle OUTSIDE the React root — createRoot clears
|
|
762
|
+
// the container's children on render, which used to delete the handle.
|
|
692
763
|
container = document.createElement("div");
|
|
693
764
|
container.dataset.dshLogcatView = "";
|
|
694
765
|
container.className = "dsh-logcat-view";
|
|
@@ -699,8 +770,11 @@ window.__ModuleLoader__.load({
|
|
|
699
770
|
handle.title = "拖拽调整宽度(双击重置)";
|
|
700
771
|
handle.addEventListener("mousedown", onDown);
|
|
701
772
|
container.appendChild(handle);
|
|
773
|
+
const panel = document.createElement("div");
|
|
774
|
+
panel.className = "dsh-logcat-panel";
|
|
775
|
+
container.appendChild(panel);
|
|
702
776
|
document.body.appendChild(container);
|
|
703
|
-
root = createRoot(
|
|
777
|
+
root = createRoot(panel);
|
|
704
778
|
root.render(h(LogcatPanel, { controller }));
|
|
705
779
|
};
|
|
706
780
|
|
package/lib/index.js
CHANGED
|
@@ -543,6 +543,36 @@ function writeJson(res, status, body) {
|
|
|
543
543
|
/** The one WebSocket server for logcat fan-out. */
|
|
544
544
|
const streamWss = new WebSocketServer({ noServer: true })
|
|
545
545
|
|
|
546
|
+
/** One-shot device info + resource stats for the panel (single adb round-trip). */
|
|
547
|
+
async function collectStats(engine, serial) {
|
|
548
|
+
const sh = 'getprop ro.product.model; getprop ro.product.brand; getprop ro.build.version.release; getprop ro.build.version.sdk; getprop ro.product.cpu.abi; wm size; grep -E "MemTotal|MemFree" /proc/meminfo; dumpsys battery | grep -E "level:|status:"; top -b -n 1 | head -3'
|
|
549
|
+
const result = await runAdbFull(engine.adb, ['-s', serial, 'shell', sh], 20000)
|
|
550
|
+
const lines = (result.stdout ?? '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
551
|
+
const get = (i) => lines[i] ?? ''
|
|
552
|
+
const field = (prefix) => { const l = lines.find((x) => x.startsWith(prefix)); return l ? l.slice(prefix.length).trim() : undefined }
|
|
553
|
+
const kb = (prefix) => { const v = field(prefix); return v ? Number.parseInt(v, 10) || undefined : undefined }
|
|
554
|
+
const totalMemKb = kb('MemTotal') ?? 0
|
|
555
|
+
const freeMemKb = kb('MemFree') ?? 0
|
|
556
|
+
const usedMemKb = totalMemKb > 0 ? totalMemKb - freeMemKb : 0
|
|
557
|
+
return {
|
|
558
|
+
ready: true,
|
|
559
|
+
serial,
|
|
560
|
+
model: get(0) || undefined,
|
|
561
|
+
brand: get(1) || undefined,
|
|
562
|
+
androidVersion: get(2) || undefined,
|
|
563
|
+
sdk: get(3) || undefined,
|
|
564
|
+
abi: get(4) || undefined,
|
|
565
|
+
resolution: (lines.find((l) => l.startsWith('Physical size')) ?? '').replace('Physical size: ', '') || undefined,
|
|
566
|
+
totalMemKb,
|
|
567
|
+
freeMemKb,
|
|
568
|
+
usedMemKb,
|
|
569
|
+
memPct: totalMemKb > 0 ? Math.round((usedMemKb / totalMemKb) * 100) : undefined,
|
|
570
|
+
batteryLevel: kb('level'),
|
|
571
|
+
batteryStatus: field('status'),
|
|
572
|
+
cpu: lines.find((l) => /%cpu|Cpu\(s\)|CPU:/i.test(l)) ?? undefined,
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
546
576
|
/** Build every /api/dsh-logcat route plus the stream upgrade. */
|
|
547
577
|
function makeRoutes(engine) {
|
|
548
578
|
const routes = [
|
|
@@ -636,6 +666,26 @@ function makeRoutes(engine) {
|
|
|
636
666
|
writeJson(res, 200, { package: pkg })
|
|
637
667
|
},
|
|
638
668
|
},
|
|
669
|
+
{
|
|
670
|
+
kind: 'exact',
|
|
671
|
+
path: API_BASE + '/stats',
|
|
672
|
+
handler: async (req, res) => {
|
|
673
|
+
if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
|
|
674
|
+
if ((req.method ?? 'GET') !== 'GET') { writeJson(res, 405, { error: 'method not allowed' }); return }
|
|
675
|
+
if (engine.adb === null) { writeJson(res, 200, { ready: false }); return }
|
|
676
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
677
|
+
const requested = url.searchParams.get('serial') ?? ''
|
|
678
|
+
const serial = requested !== '' && engine.devices.has(requested)
|
|
679
|
+
? requested
|
|
680
|
+
: [...engine.devices.keys()].find((s) => engine.devices.get(s).state === 'device') ?? ''
|
|
681
|
+
if (serial === '') { writeJson(res, 200, { ready: false }); return }
|
|
682
|
+
try {
|
|
683
|
+
writeJson(res, 200, await collectStats(engine, serial))
|
|
684
|
+
} catch (error) {
|
|
685
|
+
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
},
|
|
639
689
|
{
|
|
640
690
|
kind: 'exact',
|
|
641
691
|
path: API_BASE + '/screenshot',
|
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.5.0-preview.
|
|
4
|
+
"version": "0.5.0-preview.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|