@windypro-rourou/dsh-logcat 0.2.7 → 0.2.9
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/README.md +4 -2
- package/lib/client.js +37 -1
- package/lib/index.js +85 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,8 +4,10 @@ DSH Web GUI 的安卓实机调试面板(类似 Android Studio 的 Logcat 视
|
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
- **自动连接**:探测本机 adb(`ANDROID_HOME` / `ANDROID_SDK_ROOT` / 默认 `%LOCALAPPDATA%\Android\Sdk` / PATH
|
|
8
|
-
|
|
7
|
+
- **自动连接**:探测本机 adb(`ANDROID_HOME` / `ANDROID_SDK_ROOT` / 默认 `%LOCALAPPDATA%\Android\Sdk` / PATH /
|
|
8
|
+
`~/.dsh/adb`),每 2 秒轮询 `adb devices -l`;检测到处于调试模式的设备**自动附加 logcat 流**(`-v threadtime`),无需打开面板。
|
|
9
|
+
- **一键安装 adb**:未找到 adb 时,面板状态栏显示「一键安装 adb」按钮 —— 从 USTC 镜像(Google 官方兜底)
|
|
10
|
+
下载 platform-tools 解压到 `~/.dsh/adb` 并自动接入,开箱即用(约 10MB)。
|
|
9
11
|
- **实时日志**:WebSocket 推送,每设备保留最近 2000 行环形缓冲;断线自动重连(指数退避)。
|
|
10
12
|
- **Logcat 面板**(侧边栏「Logcat」入口,右侧抽屉,**宽度可拖拽调整并记忆**):
|
|
11
13
|
- 设备下拉(显示型号/序列号/状态,记住上次选择)
|
package/lib/client.js
CHANGED
|
@@ -110,6 +110,7 @@ window.__ModuleLoader__.load({
|
|
|
110
110
|
const [paused, setPaused] = useState(false);
|
|
111
111
|
const [currentPackage, setCurrentPackage] = useState("");
|
|
112
112
|
const [pkgInput, setPkgInput] = useState("");
|
|
113
|
+
const [installingAdb, setInstallingAdb] = useState(false);
|
|
113
114
|
const [level, setLevel] = useState("");
|
|
114
115
|
const [keyword, setKeyword] = useState("");
|
|
115
116
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
@@ -329,6 +330,30 @@ window.__ModuleLoader__.load({
|
|
|
329
330
|
.catch(() => { /* device offline etc. */ });
|
|
330
331
|
};
|
|
331
332
|
|
|
333
|
+
const installAdb = () => {
|
|
334
|
+
setInstallingAdb(true);
|
|
335
|
+
fetch(API_BASE + "/install-adb", { method: "POST" })
|
|
336
|
+
.then((res) => res.json())
|
|
337
|
+
.then((body) => {
|
|
338
|
+
if (body?.ok !== true) {
|
|
339
|
+
window.alert("adb 安装失败:" + (body?.error ?? "未知错误"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Refresh status so the panel picks up the freshly installed adb.
|
|
343
|
+
return fetch(API_BASE + "/status")
|
|
344
|
+
.then((res) => res.json())
|
|
345
|
+
.then((b) => {
|
|
346
|
+
setAdbPath(b.adbPath ?? "");
|
|
347
|
+
setAdbReady(b.ready === true);
|
|
348
|
+
setDevices(b.devices ?? []);
|
|
349
|
+
setStreaming(b.streaming ?? []);
|
|
350
|
+
setCurrentPackage(b.currentPackage ?? "");
|
|
351
|
+
});
|
|
352
|
+
})
|
|
353
|
+
.catch(() => { window.alert("adb 安装失败:网络错误"); })
|
|
354
|
+
.finally(() => setInstallingAdb(false));
|
|
355
|
+
};
|
|
356
|
+
|
|
332
357
|
return h("div", { className: "lc-panel" },
|
|
333
358
|
h("div", { className: "lc-header" },
|
|
334
359
|
h("button", { type: "button", className: "lc-back", onClick: () => controller.close() },
|
|
@@ -407,7 +432,18 @@ window.__ModuleLoader__.load({
|
|
|
407
432
|
: h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef }),
|
|
408
433
|
),
|
|
409
434
|
h("div", { className: "lc-status" },
|
|
410
|
-
h("span", null,
|
|
435
|
+
h("span", null,
|
|
436
|
+
h("b", null, adbReady ? "adb 就绪" : "未找到 adb"),
|
|
437
|
+
" · " + (adbPath || "—"),
|
|
438
|
+
!adbReady
|
|
439
|
+
? h("button", {
|
|
440
|
+
type: "button",
|
|
441
|
+
className: "lc-btn",
|
|
442
|
+
style: { marginLeft: 8, padding: "2px 8px" },
|
|
443
|
+
title: "从 Google 官方下载 platform-tools 到 ~/.dsh/adb 并接入(约 10MB)",
|
|
444
|
+
onClick: installAdb,
|
|
445
|
+
}, installingAdb ? "安装中…" : "一键安装 adb")
|
|
446
|
+
: null),
|
|
411
447
|
h("span", null, "设备 " + devices.length + " · 在线 " + devices.filter((d) => d.state === "device").length),
|
|
412
448
|
h("span", null, "显示 " + filtered.length + " / 缓冲 " + entries.length + " 行"),
|
|
413
449
|
currentPackage !== ""
|
package/lib/index.js
CHANGED
|
@@ -55,7 +55,12 @@ function logcatGuidance(engine) {
|
|
|
55
55
|
|
|
56
56
|
/** ---------------------------------------------------------------- adb */
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/** Where the plugin can self-install adb platform-tools (~/.dsh/adb). */
|
|
59
|
+
function bundledAdbRoot() {
|
|
60
|
+
return join(homedir(), '.dsh', 'adb')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Candidate adb(.exe) locations, in probe order. */
|
|
59
64
|
function adbCandidates() {
|
|
60
65
|
const list = []
|
|
61
66
|
const envs = [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]
|
|
@@ -65,9 +70,69 @@ function adbCandidates() {
|
|
|
65
70
|
const sdk = join(homedir(), 'AppData', 'Local', 'Android', 'Sdk')
|
|
66
71
|
list.push(join(sdk, 'platform-tools', 'adb.exe'))
|
|
67
72
|
list.push(join(sdk, 'platform-tools', 'adb'))
|
|
73
|
+
// Plugin-managed install location (one-click setup on first run).
|
|
74
|
+
list.push(join(bundledAdbRoot(), 'platform-tools', 'adb.exe'))
|
|
75
|
+
list.push(join(bundledAdbRoot(), 'platform-tools', 'adb'))
|
|
68
76
|
return list
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
/** Official platform-tools zip mirrors for this host, in preference order. */
|
|
80
|
+
function platformToolsUrls() {
|
|
81
|
+
const file = {
|
|
82
|
+
win32: 'platform-tools-latest-windows.zip',
|
|
83
|
+
darwin: 'platform-tools-latest-darwin.zip',
|
|
84
|
+
}[process.platform] ?? 'platform-tools-latest-linux.zip'
|
|
85
|
+
return [
|
|
86
|
+
'https://mirrors.ustc.edu.cn/android/repository/' + file,
|
|
87
|
+
'https://dl.google.com/android/repository/' + file,
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Download and unpack platform-tools into ~/.dsh/adb. Resolves the adb path. */
|
|
92
|
+
export async function installPlatformTools() {
|
|
93
|
+
const { mkdir, rm, writeFile } = await import('node:fs/promises')
|
|
94
|
+
const { tmpdir } = await import('node:os')
|
|
95
|
+
const dest = bundledAdbRoot()
|
|
96
|
+
await mkdir(dest, { recursive: true })
|
|
97
|
+
const zipPath = join(tmpdir(), `platform-tools-${Date.now()}.zip`)
|
|
98
|
+
let downloaded = false
|
|
99
|
+
let lastError = null
|
|
100
|
+
for (const url of platformToolsUrls()) {
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(180000) })
|
|
103
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
104
|
+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
|
|
105
|
+
downloaded = true
|
|
106
|
+
break
|
|
107
|
+
} catch (error) {
|
|
108
|
+
lastError = error
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!downloaded) throw new Error(`download failed from all mirrors: ${lastError?.message ?? 'unknown'}`)
|
|
112
|
+
const exe = process.platform === 'win32' ? 'adb.exe' : 'adb'
|
|
113
|
+
try {
|
|
114
|
+
if (process.platform === 'win32') {
|
|
115
|
+
// PowerShell ships with Windows; Expand-Archive unzips natively.
|
|
116
|
+
await new Promise((resolve, reject) => {
|
|
117
|
+
execFile('powershell', ['-NoProfile', '-Command', `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${dest}'`], { timeout: 180000, windowsHide: true }, (error) => {
|
|
118
|
+
if (error) reject(new Error('unzip failed: ' + (error.message ?? String(error))))
|
|
119
|
+
else resolve()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
} else {
|
|
123
|
+
await new Promise((resolve, reject) => {
|
|
124
|
+
execFile('unzip', ['-o', zipPath, '-d', dest], { timeout: 180000 }, (error) => {
|
|
125
|
+
if (error) reject(new Error('unzip failed: ' + (error.message ?? String(error))))
|
|
126
|
+
else resolve()
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
await rm(zipPath, { force: true })
|
|
132
|
+
}
|
|
133
|
+
return join(dest, 'platform-tools', exe)
|
|
134
|
+
}
|
|
135
|
+
|
|
71
136
|
/** Run one short adb command, returning stdout (or null on failure). */
|
|
72
137
|
function runAdb(adb, args, timeoutMs = 8000) {
|
|
73
138
|
return new Promise((resolve) => {
|
|
@@ -415,9 +480,28 @@ function makeRoutes(engine) {
|
|
|
415
480
|
ready: engine.adb !== null,
|
|
416
481
|
devices: engine.deviceList(),
|
|
417
482
|
streaming: [...engine.streams.keys()],
|
|
483
|
+
currentPackage: engine.currentPackage,
|
|
418
484
|
})
|
|
419
485
|
},
|
|
420
486
|
},
|
|
487
|
+
{
|
|
488
|
+
kind: 'exact',
|
|
489
|
+
path: API_BASE + '/install-adb',
|
|
490
|
+
handler: async (req, res) => {
|
|
491
|
+
if (!isLoopbackRequest(req)) { writeJson(res, 403, { error: 'forbidden: loopback-only' }); return }
|
|
492
|
+
if ((req.method ?? 'GET') !== 'POST') { writeJson(res, 405, { error: 'method not allowed' }); return }
|
|
493
|
+
if (engine.adb !== null) { writeJson(res, 200, { ok: true, already: true, adbPath: engine.adb }); return }
|
|
494
|
+
try {
|
|
495
|
+
const adbPath = await installPlatformTools()
|
|
496
|
+
engine.adb = adbPath
|
|
497
|
+
engine.adbVersion = (await runAdb(adbPath, ['version']))?.split(/\r?\n/)[0] ?? ''
|
|
498
|
+
engine.startPolling()
|
|
499
|
+
writeJson(res, 200, { ok: true, already: false, adbPath })
|
|
500
|
+
} catch (error) {
|
|
501
|
+
writeJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
},
|
|
421
505
|
{
|
|
422
506
|
kind: 'exact',
|
|
423
507
|
path: API_BASE + '/exec',
|
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.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|