dsh-adb 1.2.0 → 1.3.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/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/lib/index.js +3 -1
- package/lib/tools/wait.d.ts +42 -0
- package/lib/tools/wait.js +144 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ Topics: `dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
|
|
|
40
40
|
| `adb_perf_baseline` | Perf regression: save a snapshot as a baseline (label/tags), compare current state and get a numeric diff (PSS, janky %, percentiles), list/delete baselines (stored locally under `baselineDir`) |
|
|
41
41
|
| `adb_crash_report` | One-call crash scene: parsed logcat crash buffer + dropbox excerpt + process state + memory summary |
|
|
42
42
|
| `adb_device_report` | One-click health report: device identity + top-RSS processes + crash buffer (real crashes w/ stacks vs. boot markers) + W/E/F logcat aggregated by tag + storage + health verdict; each section degrades independently; persisted under `reportDir` |
|
|
43
|
+
| `adb_wait_for` | Wait until a device condition holds — device-online / boot-complete / process appeared / logcat keyword — polling up to a budget, instead of sleeping a fixed number of seconds; returns `matched:false` on timeout |
|
|
43
44
|
|
|
44
45
|
Errors are structured `AdbError` with stable codes: `ADB_NOT_FOUND`, `ADB_UNAVAILABLE`, `DEVICE_NOT_FOUND`, `NO_DEVICES`, `CONNECT_FAILED`, `INSTALL_FAILED`, `ADB_EXIT_<code>`, etc.
|
|
45
46
|
|
package/README.zh-CN.md
CHANGED
|
@@ -40,6 +40,7 @@ Topics:`dsh-plugin` `dsh` `adb` `android` `automotive` `bench`
|
|
|
40
40
|
| `adb_perf_baseline` | 性能回归:快照存基线(label/tags)、与当前状态数值对比(PSS/卡顿率/百分位)、list/delete(本地存储,`baselineDir`) |
|
|
41
41
|
| `adb_crash_report` | 崩溃现场一键采集:crash buffer 解析 + dropbox 摘录 + 进程状态 + 内存摘要 |
|
|
42
42
|
| `adb_device_report` | 一键体检:设备信息 + Top RSS 进程 + 崩溃缓冲(真实崩溃带堆栈/启动标记分类)+ W/E/F 日志按 tag 聚合 + 存储用量 + 健康结论;每节独立降级;落盘到 `reportDir` |
|
|
43
|
+
| `adb_wait_for` | 等待原语:等设备上线 / 启动完成 / 进程出现 / logcat 出现关键字,轮询到预算上限,替代盲目 sleep;超时返回 `matched:false` |
|
|
43
44
|
|
|
44
45
|
错误码:`ADB_NOT_FOUND`、`ADB_UNAVAILABLE`、`DEVICE_NOT_FOUND`、`NO_DEVICES`、`CONNECT_FAILED`、`INSTALL_FAILED`、`ADB_EXIT_<code>` 等,均为结构化 `AdbError`。
|
|
45
46
|
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerInstallTool } from './tools/install.js';
|
|
|
8
8
|
import { registerLogcatTool } from './tools/logcat.js';
|
|
9
9
|
import { registerPerfTool } from './tools/perf.js';
|
|
10
10
|
import { registerPerfBaselineTool } from './tools/perf-baseline.js';
|
|
11
|
+
import { registerWaitTool } from './tools/wait.js';
|
|
11
12
|
import { registerRpc } from './rpc.js';
|
|
12
13
|
import { registerSkills } from './skill.js';
|
|
13
14
|
export const name = 'dsh-adb';
|
|
@@ -31,10 +32,11 @@ export function apply(ctx, config) {
|
|
|
31
32
|
registerPerfBaselineTool(ctx, cfg, config.baselineDir ?? DEFAULT_BASELINE_DIR);
|
|
32
33
|
registerCrashReportTool(ctx, cfg);
|
|
33
34
|
registerDeviceReportTool(ctx, cfg, reportDir);
|
|
35
|
+
registerWaitTool(ctx, cfg);
|
|
34
36
|
registerSkills(ctx);
|
|
35
37
|
// The RPC channel needs the client connection, which mounts after this
|
|
36
38
|
// plugin starts in web compositions; register lazily so headless profiles
|
|
37
39
|
// (no connection) stay unaffected.
|
|
38
40
|
ctx.inject(['connection'], (readyCtx) => registerRpc(readyCtx, cfg, reportDir));
|
|
39
|
-
ctx.logger.info('[dsh-adb] loaded:
|
|
41
|
+
ctx.logger.info('[dsh-adb] loaded: 11 tools + web device panel rpc');
|
|
40
42
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type AdbConfig } from '../adb.js';
|
|
3
|
+
import { type AdbDevice } from '../parsers/devices.js';
|
|
4
|
+
import { type LogcatEntry } from '../parsers/logcat.js';
|
|
5
|
+
import { type ProcessEntry } from '../parsers/sysinfo.js';
|
|
6
|
+
/**
|
|
7
|
+
* adb_wait_for: wait until a device reaches a condition, instead of sleeping a
|
|
8
|
+
* fixed number of seconds. Conditions: device-online, boot-complete, process
|
|
9
|
+
* (a process appears), logcat-pattern (a keyword appears). Polls at
|
|
10
|
+
* `intervalMs` until `timeoutMs`, then returns `matched: false` (not an error)
|
|
11
|
+
* so the agent can react to the timeout instead of guessing.
|
|
12
|
+
*/
|
|
13
|
+
export type WaitCondition = 'device-online' | 'boot-complete' | 'process' | 'logcat-pattern';
|
|
14
|
+
export interface WaitArgs {
|
|
15
|
+
serial?: string;
|
|
16
|
+
condition: WaitCondition;
|
|
17
|
+
/** Substring matched against process names (condition=process) or logcat tag/message (condition=logcat-pattern). */
|
|
18
|
+
pattern?: string;
|
|
19
|
+
/** Overall wait budget in milliseconds; defaults to 30000, capped at 300000. */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Poll interval in milliseconds; defaults to 1000. */
|
|
22
|
+
intervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface WaitResult {
|
|
25
|
+
condition: WaitCondition;
|
|
26
|
+
matched: boolean;
|
|
27
|
+
waitedMs: number;
|
|
28
|
+
attempts: number;
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
/** device-online: the serial is present in `adb devices -l` with state `device`. */
|
|
32
|
+
export declare function checkDeviceOnline(devices: AdbDevice[], serial?: string): boolean;
|
|
33
|
+
/** boot-complete: `getprop sys.boot_completed` output is exactly "1". */
|
|
34
|
+
export declare function checkBootComplete(getpropOut: string): boolean;
|
|
35
|
+
/** process: any process name contains the pattern. */
|
|
36
|
+
export declare function checkProcessPresent(processes: ProcessEntry[], pattern: string): boolean;
|
|
37
|
+
/** logcat-pattern: any entry's tag or message contains the keyword. */
|
|
38
|
+
export declare function checkLogcatKeyword(entries: LogcatEntry[], keyword: string): boolean;
|
|
39
|
+
/** Wait until the condition holds or the budget expires (returns matched:false on timeout). */
|
|
40
|
+
export declare function waitForCondition(ctx: Context, cfg: AdbConfig, signal: AbortSignal, args: WaitArgs): Promise<WaitResult>;
|
|
41
|
+
/** adb_wait_for: wait until a device condition holds (online / boot complete / process / logcat keyword). */
|
|
42
|
+
export declare function registerWaitTool(ctx: Context, cfg: AdbConfig): void;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { classifyFailure, jsonOutput, runAdb } from '../adb.js';
|
|
2
|
+
import { parseDevices } from '../parsers/devices.js';
|
|
3
|
+
import { parseLogcat } from '../parsers/logcat.js';
|
|
4
|
+
import { parseProcessList } from '../parsers/sysinfo.js';
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_TIMEOUT_MS = 300_000;
|
|
7
|
+
const DEFAULT_INTERVAL_MS = 1_000;
|
|
8
|
+
// ---- Pure condition checks (unit-tested) ----
|
|
9
|
+
/** device-online: the serial is present in `adb devices -l` with state `device`. */
|
|
10
|
+
export function checkDeviceOnline(devices, serial) {
|
|
11
|
+
return devices.some((device) => {
|
|
12
|
+
if (serial !== undefined && device.serial !== serial)
|
|
13
|
+
return false;
|
|
14
|
+
return device.state === 'device';
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** boot-complete: `getprop sys.boot_completed` output is exactly "1". */
|
|
18
|
+
export function checkBootComplete(getpropOut) {
|
|
19
|
+
return getpropOut.trim() === '1';
|
|
20
|
+
}
|
|
21
|
+
/** process: any process name contains the pattern. */
|
|
22
|
+
export function checkProcessPresent(processes, pattern) {
|
|
23
|
+
return processes.some((entry) => entry.name.includes(pattern));
|
|
24
|
+
}
|
|
25
|
+
/** logcat-pattern: any entry's tag or message contains the keyword. */
|
|
26
|
+
export function checkLogcatKeyword(entries, keyword) {
|
|
27
|
+
return entries.some((entry) => entry.tag.includes(keyword) || entry.message.includes(keyword));
|
|
28
|
+
}
|
|
29
|
+
// ---- Poll loop ----
|
|
30
|
+
function sleep(ms, signal) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
if (signal?.aborted) {
|
|
33
|
+
reject(new Error('tool call aborted'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const timer = setTimeout(resolve, ms);
|
|
37
|
+
signal?.addEventListener('abort', () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
reject(new Error('tool call aborted'));
|
|
40
|
+
}, { once: true });
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Run one adb probe for the condition; returns true when satisfied. */
|
|
44
|
+
async function probe(ctx, cfg, args, signal) {
|
|
45
|
+
const serial = args.serial;
|
|
46
|
+
switch (args.condition) {
|
|
47
|
+
case 'device-online': {
|
|
48
|
+
const result = await runAdb(ctx, cfg, ['devices', '-l'], { signal, maxBytes: 1024 * 1024 });
|
|
49
|
+
if (result.exitCode !== 0)
|
|
50
|
+
throw classifyFailure(result);
|
|
51
|
+
return checkDeviceOnline(parseDevices(result.stdout), args.serial);
|
|
52
|
+
}
|
|
53
|
+
case 'boot-complete': {
|
|
54
|
+
const result = await runAdb(ctx, cfg, ['shell', 'getprop', 'sys.boot_completed'], { signal, serial });
|
|
55
|
+
if (result.exitCode !== 0)
|
|
56
|
+
throw classifyFailure(result);
|
|
57
|
+
return checkBootComplete(result.stdout);
|
|
58
|
+
}
|
|
59
|
+
case 'process': {
|
|
60
|
+
const result = await runAdb(ctx, cfg, ['shell', 'ps', '-A'], { signal, serial, maxBytes: 2 * 1024 * 1024 });
|
|
61
|
+
if (result.exitCode !== 0)
|
|
62
|
+
throw classifyFailure(result);
|
|
63
|
+
return checkProcessPresent(parseProcessList(result.stdout), args.pattern);
|
|
64
|
+
}
|
|
65
|
+
case 'logcat-pattern': {
|
|
66
|
+
const result = await runAdb(ctx, cfg, ['logcat', '-v', 'threadtime', '-d'], { signal, serial, maxBytes: 8 * 1024 * 1024 });
|
|
67
|
+
if (result.exitCode !== 0)
|
|
68
|
+
throw classifyFailure(result);
|
|
69
|
+
return checkLogcatKeyword(parseLogcat(result.stdout), args.pattern);
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`unknown condition: ${String(args.condition)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Validate args that fail fast — before the poll loop so they surface as errors, not timeouts. */
|
|
76
|
+
function validateArgs(args) {
|
|
77
|
+
if (!['device-online', 'boot-complete', 'process', 'logcat-pattern'].includes(args.condition)) {
|
|
78
|
+
throw new Error(`unknown condition: ${String(args.condition)}`);
|
|
79
|
+
}
|
|
80
|
+
if ((args.condition === 'process' || args.condition === 'logcat-pattern') && (args.pattern === undefined || args.pattern === '')) {
|
|
81
|
+
throw new Error(`condition "${args.condition}" requires a non-empty "pattern"`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Wait until the condition holds or the budget expires (returns matched:false on timeout). */
|
|
85
|
+
export async function waitForCondition(ctx, cfg, signal, args) {
|
|
86
|
+
validateArgs(args);
|
|
87
|
+
const timeoutMs = Math.min(Math.floor(args.timeoutMs ?? DEFAULT_TIMEOUT_MS), MAX_TIMEOUT_MS);
|
|
88
|
+
const intervalMs = Math.max(Math.floor(args.intervalMs ?? DEFAULT_INTERVAL_MS), 250);
|
|
89
|
+
const start = Date.now();
|
|
90
|
+
const deadline = start + timeoutMs;
|
|
91
|
+
let attempts = 0;
|
|
92
|
+
let lastProbe;
|
|
93
|
+
while (true) {
|
|
94
|
+
attempts++;
|
|
95
|
+
try {
|
|
96
|
+
const satisfied = await probe(ctx, cfg, args, signal);
|
|
97
|
+
if (satisfied) {
|
|
98
|
+
return { condition: args.condition, matched: true, waitedMs: Date.now() - start, attempts };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// A transient probe failure (device offline mid-wait) is not terminal:
|
|
103
|
+
// keep polling until the budget, but record the reason.
|
|
104
|
+
lastProbe = error instanceof Error ? error.message : String(error);
|
|
105
|
+
}
|
|
106
|
+
if (Date.now() >= deadline) {
|
|
107
|
+
return {
|
|
108
|
+
condition: args.condition,
|
|
109
|
+
matched: false,
|
|
110
|
+
waitedMs: timeoutMs,
|
|
111
|
+
attempts,
|
|
112
|
+
...(lastProbe !== undefined ? { reason: lastProbe } : { reason: `condition not met within ${timeoutMs}ms` }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
await sleep(Math.min(intervalMs, deadline - Date.now()), signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** adb_wait_for: wait until a device condition holds (online / boot complete / process / logcat keyword). */
|
|
119
|
+
export function registerWaitTool(ctx, cfg) {
|
|
120
|
+
ctx.tools.register({
|
|
121
|
+
name: 'adb_wait_for',
|
|
122
|
+
description: 'Wait until a device reaches a condition, then return — instead of sleeping a fixed number of seconds. Conditions: device-online (device is in `adb devices` with state `device`), boot-complete (sys.boot_completed=1), process (a process whose name contains `pattern` appears in ps), logcat-pattern (a keyword appears in logcat tag/message). Polls every `intervalMs` up to `timeoutMs`; on timeout returns matched:false (not an error) so you can react. Use it to sequence multi-step device flows: install → wait for process → snapshot.',
|
|
123
|
+
parameters: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
required: ['condition'],
|
|
127
|
+
properties: {
|
|
128
|
+
condition: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
enum: ['device-online', 'boot-complete', 'process', 'logcat-pattern'],
|
|
131
|
+
description: 'Which condition to wait for.',
|
|
132
|
+
},
|
|
133
|
+
serial: { type: 'string', description: 'Target device serial; defaults to the plugin defaultSerial. For device-online, omit serial to wait for ANY device to come online.' },
|
|
134
|
+
pattern: { type: 'string', description: 'Required for process (process-name substring) and logcat-pattern (keyword).' },
|
|
135
|
+
timeoutMs: { type: 'integer', description: 'Overall wait budget in milliseconds; defaults to 30000, capped at 300000.' },
|
|
136
|
+
intervalMs: { type: 'integer', description: 'Poll interval in milliseconds; defaults to 1000, minimum 250.' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
output: jsonOutput(),
|
|
140
|
+
async execute(args, exec) {
|
|
141
|
+
return waitForCondition(ctx, cfg, exec.signal, args);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-adb",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots, perf baselines, crash reports, one-click device health reports, web device panel (autocomplete, live logcat, profiler)",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "ADB device & bench operations for DeepSeek Harness: device discovery, structured logcat, apk install, file pull/push, performance snapshots, perf baselines, crash reports, one-click device health reports, condition waits, web device panel (autocomplete, live logcat, profiler)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|