@windypro-rourou/dsh-logcat 0.5.0-preview.2 → 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 +92 -24
- package/lib/index.js +50 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -35,50 +35,62 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
[data-dsh-logcat-entry] .lc-entry-label { font-size: 13px; line-height: 1.2; opacity: .92; }
|
|
36
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 {
|
|
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
|
+
}
|
|
39
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; }
|
|
40
|
-
.dsh-logcat-resize::before { content: ""; width: 3px; height: 48px; border-radius: 2px; background: rgba(
|
|
41
|
-
.dsh-logcat-resize:hover::before, .dsh-logcat-resize[data-drag]::before { background: rgba(
|
|
42
|
-
.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)); }
|
|
43
49
|
.lc-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
|
44
|
-
.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; }
|
|
45
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; }
|
|
46
|
-
.lc-back:hover { background: rgba(
|
|
47
|
-
.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; }
|
|
48
54
|
.lc-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
49
|
-
.lc-dot.on { background: #4caf50; }
|
|
55
|
+
.lc-dot.on { background: #4caf50; box-shadow: 0 0 6px rgba(76,175,80,.6); }
|
|
50
56
|
.lc-dot.off { background: #9e9e9e; }
|
|
51
57
|
.lc-dot.warn { background: #ff9800; }
|
|
52
|
-
.lc-
|
|
53
|
-
.lc-
|
|
54
|
-
.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); }
|
|
55
67
|
.lc-search { flex: 1; min-width: 120px; max-width: 320px; }
|
|
56
|
-
.lc-levels { display: flex; gap: 2px; }
|
|
57
|
-
.lc-level { border:
|
|
58
|
-
.lc-level:hover { background: rgba(
|
|
59
|
-
.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); }
|
|
60
72
|
.lc-level.v { color: #9e9e9e; } .lc-level.d { color: #4fc3f7; } .lc-level.i { color: #4caf50; }
|
|
61
73
|
.lc-level.w { color: #fbc02d; } .lc-level.e { color: #ef5350; } .lc-level.f { color: #ab47bc; }
|
|
62
|
-
.lc-btn { border: 1px solid rgba(
|
|
63
|
-
.lc-btn:hover { background: rgba(
|
|
64
|
-
.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; }
|
|
65
77
|
.lc-body { flex: 1; min-height: 0; position: relative; overflow: hidden; }
|
|
66
78
|
.lc-log { position: absolute; inset: 0; overflow: auto; font-family: Consolas, "Cascadia Mono", "Courier New", monospace; font-size: 12px; line-height: 20px; }
|
|
67
79
|
.lc-log-inner { position: relative; }
|
|
68
80
|
.lc-line { position: absolute; left: 0; right: 0; padding: 0 12px; white-space: pre; overflow: hidden; text-overflow: ellipsis; cursor: default; }
|
|
69
|
-
.lc-line:hover { background: rgba(
|
|
70
|
-
.lc-line .ts { color:
|
|
71
|
-
.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; }
|
|
72
84
|
.lc-line .lv { display: inline-block; width: 14px; text-align: center; font-weight: 700; margin-right: 6px; }
|
|
73
85
|
.lc-line .lv.V { color: #9e9e9e; } .lc-line .lv.D { color: #4fc3f7; } .lc-line .lv.I { color: #4caf50; }
|
|
74
86
|
.lc-line .lv.W { color: #fbc02d; } .lc-line .lv.E { color: #ef5350; } .lc-line .lv.F { color: #ab47bc; }
|
|
75
87
|
.lc-line .tag { color: #29b6f6; margin-right: 8px; }
|
|
76
|
-
.lc-line.cont .msg { padding-left: 44px; color:
|
|
88
|
+
.lc-line.cont .msg { padding-left: 44px; color: light-dark(#9aa0a8, #7c838d); }
|
|
77
89
|
.lc-line.crash { background: rgba(239,83,80,.14); box-shadow: inset 2px 0 0 #ef5350; }
|
|
78
90
|
.lc-line.crash .tag { color: #ef5350; font-weight: 700; }
|
|
79
91
|
.lc-line.crash .msg { color: #ef5350; }
|
|
80
|
-
.lc-empty { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color:
|
|
81
|
-
.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); }
|
|
82
94
|
.lc-status b { font-weight: 600; color: inherit; }
|
|
83
95
|
`;
|
|
84
96
|
//#endregion
|
|
@@ -122,6 +134,7 @@ window.__ModuleLoader__.load({
|
|
|
122
134
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
|
123
135
|
const [updateKind, setUpdateKind] = useState("");
|
|
124
136
|
const [updateHint, setUpdateHint] = useState("");
|
|
137
|
+
const [stats, setStats] = useState(null);
|
|
125
138
|
const [level, setLevel] = useState("");
|
|
126
139
|
const [keyword, setKeyword] = useState("");
|
|
127
140
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
@@ -271,6 +284,34 @@ window.__ModuleLoader__.load({
|
|
|
271
284
|
.catch(() => { /* host not up yet */ });
|
|
272
285
|
}, []);
|
|
273
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
|
+
|
|
274
315
|
const filtered = useMemo(() => {
|
|
275
316
|
const needle = keyword.trim().toLowerCase();
|
|
276
317
|
return entries.filter((e) => {
|
|
@@ -392,6 +433,33 @@ window.__ModuleLoader__.load({
|
|
|
392
433
|
h("option", { key: d.serial, value: d.serial },
|
|
393
434
|
(d.model !== "" ? d.model + " · " : "") + d.serial + " [" + d.state + "]"))),
|
|
394
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
|
+
),
|
|
395
463
|
h("div", { className: "lc-toolbar" },
|
|
396
464
|
h("div", { className: "lc-levels", role: "group", "aria-label": "日志级别" },
|
|
397
465
|
h("button", {
|
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": {
|