ADB 模式:连接与设备捕获流程
ADB 模式是 SpinningMomo 通过外部 adb 可执行文件与 Android 设备/模拟器建立连接、枚举设备并在设备侧运行捕获守护进程(momo-capture)以获取画面与音频的通道。本页覆盖连接建立、设备枚举、命令执行与设备捕获启动的完整链路。
目的与范围(Purpose and Scope)
本页覆盖:
- 桌面端 ADB 客户端模块
features::adb_mode::adb(src/features/adb_mode/adb_client.hpp / adb_client.cpp)的全部公开 API:可执行文件解析、命令执行、设备枚举、TCP 连接/断开。 - 对应的 RPC 端点层(src/core/rpc/endpoints/adb_mode/)与 Web 设置界面入口(
AdbModeContent.vue、AdbDeviceInput.vue)在本链路中的位置。 - 设备侧捕获守护进程的启动脚本
scripts/run-android.js及其与momo-capture.jar的衔接方式。
本页不覆盖(留给姊妹页/独立文档):
- 捕获守护进程 momo-capture 内部实现(VirtualDisplay 管线、音频回放捕获、编码器选择等)——见 android/capture/README.md 与
android/capture/src/com/spinningmomo/capture/目录。 - 通用框架基础设施(RPC、进程工具、worker pool)——按 AGENTS.md 的分层约定属于
core::*与ui::*。 - 其他 features(录制、截图业务逻辑、图库等)。
说明:本页对 C++ 客户端 API 的描述以
adb_client.hpp的真实声明与注释为准;RPC 端点adb_mode.cpp与types.hpp的内部实现本次未逐行展开,相关结论以文件路径与头文件契约为边界。
概述(Overview)
ADB 模式解决的问题是:Windows 桌面端不直接持有 Android 系统权限,必须借助现成的 adb 工具(通常来自模拟器自带目录,如 C:\模拟器\adb.exe)作为受控外部进程,完成三件事:
- 发现:
adb devices -l枚举当前可用设备(含序列号与描述),供 UI 选择。 - 连接:对网络设备(模拟器
127.0.0.1:16384、真机无线调试192.168.1.100:5555等)执行adb connect host:port,并区分"本模块建立的 TCP 连接"与既有连接(断开时只清理自己建立的连接)。 - 捕获:在选定设备(
-s <serial>)上推送/启动捕获守护进程momo-capture.jar,由其内部的CaptureServer输出画面(安装器注释明确指出该服务用于 "ADB JPEG screenshots")。
设计意图:错误处理采用 std::expected<T, std::string> 而非异常,且刻意区分执行器错误(启动失败/超时)与非零退出码——底层 run() 只把前者视为错误,非零退出码交由调用方解释语义;面向设备的高层封装 run_on_device() 则把非零退出码统一转换成错误。这种分层让"连接失败"和"命令本身失败"可被分别诊断。
架构(Architecture)
分层说明(与 AGENTS.md 的 core::* / features::* / ui::* 划分一致):
- Web UI 层:web/src/features/settings/components/AdbModeContent.vue 承载设置页中 ADB 模式的配置面板;web/src/components/AdbDeviceInput.vue 是复用的设备序列号输入组件。
- RPC 端点层:src/core/rpc/endpoints/adb_mode/adb_mode.cpp 是前端命令进入 C++ 侧的入口,将请求转给 features 层。
- ADB 客户端层:
features::adb_mode::adb命名空间(见下文 API 参考)是唯一与adb进程打交道的代码,集中管理超时、-s序列号注入与输出解析。 - 外部进程:
adb可执行文件由用户在配置中指定路径,客户端不内置。 - 设备侧:
momo-capture.jar由 android/capture 构建产出(Main.java入口 →CaptureServer.java服务循环 →ScreenCapture.java等捕获源),安装器 installer/Package.wxs 中将其作为 "Android capture service used by ADB JPEG screenshots" 打包分发。
核心链路 1:连接与设备枚举
客户端层的全部公开契约集中在 src/features/adb_mode/adb_client.hpp:
1namespace features::adb_mode::adb {
2
3// 解析用户指定的 ADB 路径。
4auto resolve_executable(std::string_view configured_path)
5 -> std::expected<std::filesystem::path, std::string>;
6
7// 执行 ADB 命令;只把启动失败/超时视为执行器错误,非零退出码交给调用方解释。
8auto run(const AdbConnectionConfig& config, const std::vector<std::wstring>& arguments,
9 std::chrono::milliseconds timeout = std::chrono::seconds(15))
10 -> std::expected<utils::process::CommandResult, std::string>;
11
12// 为目标设备补上 -s 序列号并执行 ADB 命令,非零退出码直接转换为错误。
13auto run_on_device(const AdbConnectionConfig& config, std::string_view serial,
14 const std::vector<std::wstring>& arguments,
15 std::chrono::milliseconds timeout = std::chrono::seconds(15))
16 -> std::expected<utils::process::CommandResult, std::string>;
17
18// 执行 adb devices -l 并把标准输出解析成设备列表。
19auto list_devices(const AdbConnectionConfig& config)
20 -> std::expected<std::vector<AdbDevice>, std::string>;
21
22// 连接配置中的 host:port(如有需要),并返回设备序列号和连接所有权。
23auto connect(const AdbConnectionConfig& config) -> std::expected<AdbConnectionResult, std::string>;
24
25// 主动尝试连接指定的 endpoint (例如 127.0.0.1:16384 或 192.168.1.100:5555)。
26auto connect_endpoint(const AdbConnectionConfig& config, std::string_view endpoint,
27 std::chrono::milliseconds timeout = std::chrono::seconds(5))
28 -> std::expected<void, std::string>;
29
30// 断开本模块建立的 TCP ADB 连接。
31auto disconnect(const AdbConnectionConfig& config, std::string_view serial)
32 -> std::expected<void, std::string>;
33
34} // namespace features::adb_mode::adbSource: adb_client.hpp
这段头文件揭示了几个关键设计决策:
- 参数以
std::wstring传递:命令行参数走宽字符,避免中文路径(如C:\模拟器\adb.exe)在 Windows 上的编码问题。 - 两级错误语义:
run()是低层原语,"进程没跑起来/超时"才算执行器错误;run_on_device()是高层封装,自动拼adb -s <serial> ...并把非零退出码折叠成std::expected错误值。 - 连接所有权:
connect()返回的AdbConnectionResult带有"连接所有权"信息(头文件注释原文),disconnect()只断开本模块建立的 TCP 连接——避免用户手动adb connect的连接被应用退出时误杀。 - 双连接入口:
connect()使用配置中既有的host:port;connect_endpoint()允许对任意 endpoint 主动探测(5 秒默认超时),注释中给出的典型值是模拟器127.0.0.1:16384与局域网真机192.168.1.100:5555。 - 默认超时 15 秒:命令执行统一 15s 超时上限,防止 adb 卡死拖垮调用线程。
连接时序
顺序上先 resolve_executable 确认可执行文件存在可用,再枚举设备,随后才建立 TCP 连接,最后一切命令都经 run_on_device 定向到具体 serial——这个次序保证了 UI 能在每一步给出精确的错误提示(路径错 / 无设备 / 连接失败 / 命令失败各自可区分)。
核心链路 2:设备捕获启动流程
连接建立后,设备侧捕获由 momo-capture 守护进程承担。仓库提供了一个独立的 Node 驱动脚本 scripts/run-android.js 展示了完整的启动编排:
1const { values } = parseArgs({ options: {
2 adb: { type: "string" }, serial: { type: "string" }, screenshot: { type: "boolean" },
3 output: { type: "string" }, help: { type: "boolean" },
4} });
5
6if (values.help) {
7 console.log('用法:node scripts/run-android.js --adb "C:\\模拟器\\adb.exe" --serial "设备序列号"');
8 console.log('或:node scripts/run-android.js --adb "C:\\模拟器\\adb.exe"');
9}
10
11if (!values.adb || !values.serial || /[\s\x00-\x1f]/.test(values.serial)) {
12 throw new Error("请明确提供 --adb 可执行文件路径和 --serial 设备序列号。");
13}
14
15const adb = path.resolve(values.adb);
16const jar = path.join(root, "build", "android", "momo-capture.jar");
17
18for (const file of [adb, jar]) {
19 if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
20 throw new Error(`文件不存在: ${file}`);
21 }
22}
23
24const result = spawnSync(adb, ["-s", values.serial, ...args], {
25 cwd: root, encoding: "utf8", timeout, maxBuffer: 8 * 1024 * 1024,
26});Source: scripts/run-android.js
该脚本体现了与 C++ 客户端一致的防御性设计:
- 严格参数校验:
--serial使用正则/[\s\x00-\x1f]/拒绝空白与控制字符——序列号随后会被拼进adb -s <serial>命令行,这是防止参数注入的第一道闸门。 - 前置存在性检查:对
adb可执行文件与build/android/momo-capture.jar同时做existsSync+isFile校验,把"文件缺失"从运行期失败提前为启动期错误。 - 子进程约束:
spawnSync设置timeout(默认 15 秒,与 C++ 侧run()的 15s 默认值一致)和maxBuffer: 8MB,防止单条 adb 命令无输出或海量输出拖垮驱动方。
捕获守护进程侧
momo-capture.jar 的入口链为 Main.java → CaptureServer.java,内部由 ScreenCapture.java(画面)、AudioPlaybackCapture.java / AudioDirectCapture.java(音频)、AudioEncoder.java(编码)、RecordSession.java / ScreenRecorder.java(录制会话)等组成,通过 VirtualDisplay 管线取流(详见 android/capture/README.md)。其协议层 CaptureProtocol.java 与产物 CodecReport.java、Workarounds.java、FakeContext.java 负责协议帧、编码器能力上报与厂商兼容处理。安装器将其作为常驻 Android 服务打包,注释明确其用途为 "ADB JPEG screenshots":
<!-- Android capture service used by ADB JPEG screenshots -->
<Fragment>Source: installer/Package.wxs
端到端流程
配置选项
配置载体为 AdbConnectionConfig(定义于 src/features/adb_mode/types.hpp,本次未逐行读取),结合头文件契约与脚本参数可确定的配置面如下:
| 配置项 | 类型 | 默认 / 约束 | 说明 |
|---|---|---|---|
| ADB 可执行文件路径 | string(传给 resolve_executable) | 必填,示例 C:\模拟器\adb.exe | 用户指定的外部 adb 工具路径;不内置二进制 |
| 设备序列号 serial | string | 必填,禁空白/控制字符(正则 /[\s\x00-\x1f]/) | 用于 adb -s <serial> 定向执行 |
| 网络 endpoint(host:port) | string,形如 127.0.0.1:16384 / 192.168.1.100:5555 | 可选 | 模拟器或无线调试真机的 TCP ADB 端点,connect()/connect_endpoint() 使用 |
| 命令执行超时 | std::chrono::milliseconds | 15s(run/run_on_device 默认值);connect_endpoint 默认 5s | 单条 adb 命令上限,防卡死 |
| momo-capture.jar 路径 | 文件路径 | build/android/momo-capture.jar | 脚本侧由构建产物定位 |
API 参考
以下签名均出自 adb_client.hpp,命名空间 features::adb_mode::adb。
resolve_executable(configured_path: std::string_view) -> std::expected<std::filesystem::path, std::string>
解析用户配置的 ADB 路径,返回可直接启动的可执行文件路径;失败时错误字符串用于 UI 提示。设计意图:将"路径规范化/存在性校验"与"命令执行"解耦,使后续每个调用点无需重复校验。
run(config, arguments: std::vector<std::wstring>, timeout = 15s) -> std::expected<utils::process::CommandResult, std::string>
底层命令原语。只把启动失败/超时视为执行器错误,非零退出码交给调用方解释(头文件注释原文)。参数:config 连接配置;arguments 完整 adb 参数(宽字符);timeout 上限。返回:CommandResult(含退出码与输出)或错误字符串。Throws:无——全链路用 std::expected,不抛异常。
run_on_device(config, serial: std::string_view, arguments, timeout = 15s) -> std::expected<CommandResult, std::string>
自动补上 -s <serial> 的设备定向执行;非零退出码直接转换为错误。这是 UI/端点层最常用的入口,适合所有"要么成功要么报错"的命令。
list_devices(config) -> std::expected<std::vector<AdbDevice>, std::string>
执行 adb devices -l 并把标准输出解析为 AdbDevice 列表。用于设备选择下拉框。
connect(config) -> std::expected<AdbConnectionResult, std::string>
连接配置中的 host:port(如有需要),返回设备序列号和连接所有权——所有权决定退出时是否自动 disconnect。
connect_endpoint(config, endpoint: std::string_view, timeout = 5s) -> std::expected<void, std::string>
对指定 endpoint 主动尝试连接(探测式),典型用于模拟器 127.0.0.1:16384 或局域网真机 192.168.1.100:5555。
disconnect(config, serial: std::string_view) -> std::expected<void, std::string>
断开本模块建立的 TCP ADB 连接;不触碰用户手工建立的连接。
失败模式、边界与并发
基于源码证据可确认的边界行为:
- 错误分层:启动失败/超时(执行器错误)与非零退出码(命令语义错误)在
run()中被刻意区分;run_on_device()才折叠为统一错误。诊断连接问题时应优先调用run()以保留语义。 - 连接所有权边界:
connect()返回所有权标记,disconnect()据此只清理自建连接——应用异常退出不会误断用户的 adb 会话。 - 注入防护:serial 拒绝空白与控制字符(
/[\s\x00-\x1f]/),endpoint 亦由connect_endpoint显式接收,避免拼接命令行时被注入额外参数。 - 超时与缓冲上限:命令 15s、连接探测 5s、脚本侧
maxBuffer 8MB,三者共同防止单个 adb 子进程长时间占用或撑爆输出缓冲。 - 缺文件前置失败:adb 路径与 momo-capture.jar 均在执行前做存在性检查,避免半途失败留下不一致状态。
- 并发注意:
std::expected无异常路径使并发调用安全边界清晰,但connect/disconnect的所有权簿记意味着并发断开同一 serial 时需要上层(RPC 端点)串行化——头文件未声明内部互斥,保守起见应由调用方协调。
性能与运维要点
- 进程启动开销:每次
run()都 spawn 新的 adb 子进程;频繁枚举设备(如轮询)会放大进程创建成本,建议 UI 层做节流。 - 超时预算:15s 默认超时覆盖了冷启动 adb server 的场景;首次命令通常最慢(触发 adb server 启动)。
- 安装器分发:momo-capture 作为 Android 捕获服务随安装包分发(installer/Package.wxs 附近),升级 adb 相关能力时需同步更新该 Fragment。
- 扩展点:新增 adb 命令时优先基于
run_on_device()封装;若需要解释非零退出码(例如adb connect的特定输出),则下沉到run()并在调用方解析CommandResult。新增设备类型(如无线调试)只需在 UI 配置 endpoint 并复用connect_endpoint(),无需改动客户端层。
相关链接
- docs/features/adb.md 与 docs/en/features/adb.md — ADB 模式用户文档
- android/capture/README.md — momo-capture 守护进程架构、VirtualDisplay 管线与 ADB 调试
- src/core/rpc/endpoints/adb_mode/adb_mode.cpp — RPC 端点实现
- src/features/adb_mode/adb_client.hpp — 客户端契约(本页 API 参考来源)
- scripts/run-android.js — 手动驱动捕获守护进程的编排脚本
- AGENTS.md — 仓库分层约定(core / features / ui)