Repository Wiki
ChanIok/SpinningMomo

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)作为受控外部进程,完成三件事:

  1. 发现:adb devices -l 枚举当前可用设备(含序列号与描述),供 UI 选择。
  2. 连接:对网络设备(模拟器 127.0.0.1:16384、真机无线调试 192.168.1.100:5555 等)执行 adb connect host:port,并区分"本模块建立的 TCP 连接"与既有连接(断开时只清理自己建立的连接)。
  3. 捕获:在选定设备(-s <serial>)上推送/启动捕获守护进程 momo-capture.jar,由其内部的 CaptureServer 输出画面(安装器注释明确指出该服务用于 "ADB JPEG screenshots")。

设计意图:错误处理采用 std::expected<T, std::string> 而非异常,且刻意区分执行器错误(启动失败/超时)与非零退出码——底层 run() 只把前者视为错误,非零退出码交由调用方解释语义;面向设备的高层封装 run_on_device() 则把非零退出码统一转换成错误。这种分层让"连接失败"和"命令本身失败"可被分别诊断。

架构(Architecture)

Loading diagram...

分层说明(与 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:

cpp
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::adb

Source: 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 卡死拖垮调用线程。

连接时序

Loading diagram...

顺序上先 resolve_executable 确认可执行文件存在可用,再枚举设备,随后才建立 TCP 连接,最后一切命令都经 run_on_device 定向到具体 serial——这个次序保证了 UI 能在每一步给出精确的错误提示(路径错 / 无设备 / 连接失败 / 命令失败各自可区分)。

核心链路 2:设备捕获启动流程

连接建立后,设备侧捕获由 momo-capture 守护进程承担。仓库提供了一个独立的 Node 驱动脚本 scripts/run-android.js 展示了完整的启动编排:

javascript
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":

xml
<!-- Android capture service used by ADB JPEG screenshots --> <Fragment>

Source: installer/Package.wxs

端到端流程

Loading diagram...

配置选项

配置载体为 AdbConnectionConfig(定义于 src/features/adb_mode/types.hpp,本次未逐行读取),结合头文件契约与脚本参数可确定的配置面如下:

配置项类型默认 / 约束说明
ADB 可执行文件路径string(传给 resolve_executable)必填,示例 C:\模拟器\adb.exe用户指定的外部 adb 工具路径;不内置二进制
设备序列号 serialstring必填,禁空白/控制字符(正则 /[\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::milliseconds15s(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(),无需改动客户端层。

相关链接

Sources

(1 files)