Files
curios/docs/research/harmony-mtp-session.md
T

117 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Harmony Connector:MTP 会话恢复与能力探测
调研日期:2026-09-10。本文补充 [初始技术调研](harmony-connector.md),只记录源码、公开问题和建议验证路线,不声称完成了真机修复。支持范围按实际 MTP 能力判定,不以 HarmonyOS 版本、机型字符串或 USB product 字符串限制。
本次任务提供的现象:macOS、libmtp 1.1.23、libusb 1.0.30;Pura 70 Ultra 的 USB 标识为 `12d1:1101`,USB product 为 `HDC Device`,MTP friendly name 为 `HUAWEI Pura70Ultra`,MTP model 为 `HBP-AL00`。0.1.1 在 `LIBMTP_Release_Device` 的 CloseSession 等待约 61 秒后超时;界面已异步处理断开,但用户报告需要拔插才能重连。上述为主任务提供的真机信息,本研究代理未操作 USB。
## 可用于实现决策的结论
1. **libmtp 现有路径已经会 reset,不能把“新增 reset”当成修复。** `12d1:1101` 表项使用 `DEVICE_FLAGS_ANDROID_BUGS`,其中同时包含 `LONG_TIMEOUT` 和 `FORCE_RESET_ON_CLOSE`。前者选择 60 秒 USB 超时,后者在释放接口后、关闭 USB handle 前 reset。约 61 秒现象与该等待路径吻合,但仍须日志确认实际失败阶段。[设备表](https://github.com/libmtp/libmtp/blob/v1.1.23/src/music-players.h#L3290)、[标志定义](https://github.com/libmtp/libmtp/blob/v1.1.23/src/device-flags.h#L320)、[USB 实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libusb1-glue.c#L2087)
2. **OpenHarmony 开源代码存在能产生 CloseSession 无响应的明确控制流。** 它把协议成功码 `0x2001` 当成内部函数返回值,但组包调用方要求 `0`;因此在写响应前退出。该路径也未清除会话状态。它能解释同类症状,尚不能证明这台商业固件包含相同代码。完整链条见下节。
3. **`LIBMTP_Reset_Device` 不是 USB reset。** 它仅在设备广告 `ResetDevice(0x1010)` 时发送 MTP 操作;上述 OpenHarmony 实现又把该操作路由到 CloseSession 的同一实现。不能用它替代 USB 层恢复。[libmtp 实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L3633)、[OpenHarmony 路由](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation.cpp#L249)
4. **建议优先验证“短时限关闭 → 释放接口 → 一次 USB reset → 关闭 handle → 新枚举重连”。** 必须记录每阶段结果,不能静默保留会话并显示已断开。单纯更换 session ID、再加既有标志或一直重试,均没有充分依据。
## OpenHarmony CloseSession 的具体问题
此次 GitHub API 查询到的 `openharmony/multimedia_media_library` 当前提交为 `4a411de74cfaf694c14820a51a0e900b04241fe6`。以下结论固定到该提交,描述其开源实现,不将 OpenHarmony 与商业 HarmonyOS 固件等同。
### 响应没有写入的调用链
| 层次 | 所查行为 | 影响 |
| --- | --- | --- |
| 内部成功码 | `MTP_SUCCESS = 0`。 | C++ 内部处理成功应返回 0。[定义](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/frameworks/utils/include/medialibrary_errno.h#L30) |
| 协议成功码 | `MTP_OK_CODE = 0x2001`。 | 这是 MTP 响应容器的 response code。[定义](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/include/mtp_constants.h#L413) |
| CloseSession payload | `Parser` 在 session 已开时返回 `MTP_OK_CODE`;`Maker` 写入该数值后也返回 `MTP_OK_CODE`。 | 返回值与调用层的内部成功码约定不一致。[实现](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/payload_data/close_session_data.cpp#L34) |
| MtpPacket | `ParserPayload` 传播返回值;`MakerPayload`、`Maker` 只有返回 `MTP_SUCCESS` 才继续。 | CloseSession 的组包结果为 `0x2001`,被当作失败返回。[实现](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_packet.cpp#L166) |
| MtpOperation | 收包失败也尝试 `SendMakeResponsePacket`,但该方法在 `responsePacketPtr_->Maker(false) != MTP_SUCCESS` 时提前返回。 | 无论从正常还是错误响应路径进入,都会在 `Write` 之前结束。[实现](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation.cpp#L129) |
这是静态控制流分析:在所查代码和正常对象初始化前提下,可以追踪到“收到关闭请求,主机却收不到关闭响应”的路径;并非手机端日志或固件反编译结果。`CalculateSize` 也调用这个返回协议码的 `Maker`,不过无需依赖长度问题,调用方对返回值的检查已足以解释写入被跳过。
### 会话状态与重新打开
`DealRequest` 在 OpenSession 时检查 `sessionOpen`,未打开时保存参数中的 `tempSessionID`,已打开时返回 SessionAlreadyOpen;`GetOpenSession` 在成功响应时设置 `sessionOpen=true`。CloseSession 的 `Parser` 只检查已有会话,`GetCloseSession` 只创建 payload,没有将 `sessionOpen` 设为 false 或将 `sessionID` 清零;`ResetOperation` 只重置当前命令、transaction ID 和数据阶段。所查 `services/media_mtp` 源文件未找到关闭时清除这些会话字段的其他赋值。[打开处理与每次操作重置](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation.cpp#L213)、[会话响应处理](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/mtp_operation_utils.cpp#L668)、[初始状态](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/include/mtp_operation_context.h#L49)
因此,新的非零 session ID 不会自动绕过 `sessionOpen` 检查。设备端 `ResetDevice` 和 CloseSession 又使用同一个 payload 与响应处理,不能从 OperationsSupported 含 `0x1010` 推断它真的重置了会话。[payload 工厂](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/packet_payload_factory.cpp#L65)
## libmtp 的关闭、打开与身份语义
1.1.23 的 `LIBMTP_Release_Device` 返回类型是 `void`:它调用 `close_device`,然后清错误栈、释放 `PTP_USB`、`PTPParams`、存储和 device 对象。因而 Release 返回后,旧 `device`、`usbinfo` 和 USB handle 都不可继续使用;仅靠该 API 的返回值也拿不到关闭是否成功。[释放实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L2513)
关闭顺序为 `ptp_closesession → close_usb`;后者默认执行 `clear_stall → libusb_release_interface → 按标志 libusb_reset_device → libusb_close`。reset 和 release 的返回值在此没有向调用者报告。连接路径先以 5 秒时限尝试 OpenSession,遇 `PTP_ERROR_IO` 已会 reset、关闭 USB、重新初始化并重试;接受 `OK` 和 `SessionAlreadyOpened`。若回到 retry,正常设备超时可能又是 60 秒。[1.1.23 USB 流程](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libusb1-glue.c#L2304)
截至本次调研的 libmtp 当前提交 `9de46078d15d290ac5c46039937b1164b918dbf0`,CloseSession 和 OpenSession 的上述恢复路径仍存在,未发现可通过直接升级主分支获得的对应修复。[当前代码](https://github.com/libmtp/libmtp/blob/9de46078d15d290ac5c46039937b1164b918dbf0/src/libusb1-glue.c)
libmtp 使用 session 参数 `1`。`ptp_opensession` 自行将 OpenSession transaction ID 归零,并在事务后将本地 session ID 设为传入值。修改随机 ID 必须连同调用约定理解;它不会修复无响应或设备端 `sessionOpen` 不清除。代码中 InvalidTransactionID 分支先加 10,再调用会归零的 `ptp_opensession`,因此也不能把该分支视为可靠的 ID 恢复保证。[PTP 实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/ptp.c#L1364)
`12d1:1101` 的上游记录来自 Nova14 的 ID 报告,只证明这个 USB ID 已入表;它不是对 Pura 70 Ultra 的型号鉴定。USB product `HDC Device` 也不否定同一复合设备上的 MTP 接口。应用应展示实际 MTP friendly/model 信息,使用选中 USB 实例及实际接口建立会话;不能将表中的 Nova14 名称硬编码为真实机型。[上游 issue #334](https://github.com/libmtp/libmtp/issues/334)、[设备表](https://github.com/libmtp/libmtp/blob/v1.1.23/src/music-players.h#L3290)
libmtp README 本身记录了设备 CloseSession 实现不佳、首次打开成功而后续失败的历史问题;这是固件兼容问题的先例,不是当前设备根因的独立证明。[libmtp README](https://github.com/libmtp/libmtp/blob/v1.1.23/README)
## macOS USB reset 的限制与调用位置
`libusb_reset_device` 的公开契约是 USB port reset,并尝试恢复先前 configuration/alternate settings;如果重枚举后的设备改变而返回 `LIBUSB_ERROR_NOT_FOUND`,调用者应关闭旧 handle 并重新发现设备。它不是“手机已经接受新的 MTP 会话”的保证。[libusb API](https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html#gafee9c4638f1713ca5faa867948878111)
libusb 1.0.30 Darwin 后端在普通、未 capture 的 macOS 路径调用 `USBDeviceReEnumerate`,等待重枚举,再恢复状态;源码说明 10.11 起旧 `ResetDevice` 路径不再有效。它有专门的重枚举等待和返回值,不能省略检查,也不能与另一线程的 close/open/reset 并发。[Darwin reset 实现](https://github.com/libusb/libusb/blob/v1.0.30/libusb/os/darwin_usb.c#L2139)
2026-08 的上游 [issue #1923](https://github.com/libusb/libusb/issues/1923) 报告 Darwin 在接口仍 claimed 时 reset、再 claim 的旧接口泄漏,以及 restore 失败后 claim 位丢失。对应 [PR #1942](https://github.com/libusb/libusb/pull/1942) 在本次查询时仍未合并。报告包含 macOS 实验,但设备是 SEGGER J-Link、部分失败来自故障注入,不能当成本项目手机已命中该问题。它足以说明:**不能因为其他库先 reset 就无条件把本项目 reset 移到 release_interface 之前。**
建议的调用位置:
| 位置 | 判定 |
| --- | --- |
| 传输中、另一个线程仍执行 MTP 时 | 不使用;先串行收束当前事务,避免重置仍在使用的句柄。 |
| CloseSession 失败后、USB handle 尚有效 | 可以由拥有该 handle 的底层实现处理;优先释放接口后 reset,检查结果,再 close。 |
| `LIBMTP_Release_Device` 返回后,拿旧 `usbinfo` reset | 错误:对象和 handle 已释放。 |
| Release 完成后,新枚举并打开同一 USB 实例的全新 libusb handle,再 reset | 可作为受控恢复路线;这时不要先 OpenSession 再 reset。需要准确定位设备、检查返回值并重新枚举,不能仅按 VID/PID 匹配第一台。 |
| 正常关闭也无条件反复 reset | 不作为额外策略;现有 quirk 已可能 reset,重复执行增加重枚举和占用干扰。 |
在本项目固定版本的 C 边界内,关闭超时和原始 reset 状态若无法经公开 API取得,可用小范围、可重现的依赖补丁返回状态;避免在 Rust/前端按内存偏移解包私有结构。若用新 handle 做恢复,不应复用旧 bus/device address 作为永久身份;重枚举可能使它失效。[libusb 枚举与 handle 说明](https://libusb.sourceforge.io/api-1.0/group__libusb__dev.html)
## 其他 macOS MTP 工具能提供什么证据
| 实现 | 观察 | 可借鉴与边界 |
| --- | --- | --- |
| OpenMTP 的 go-mtpx/go-mtpfs | `go-mtpx` 固定到 `go-mtpfs` 提交 `1c3302b3c476`。`Close` 遇 CloseSession 错误会先 USB Reset,再 ReleaseInterface、Close;`Configure` 遇 SessionAlreadyOpened 先尝试 CloseSession/OpenSession,其余失败 reset、close、等约一秒、重新打开。 | 证明一种现实恢复路线存在,但不是这台设备或当前 libusb 1.0.30 的验证。不要直接继承“约一秒足够”的假设。[依赖](https://github.com/ganeshrvel/go-mtpx/blob/18f12db021cc4256396748dbd2996d15853d7033/go.mod)、[关闭和配置](https://github.com/ganeshrvel/go-mtpfs/blob/1c3302b3c476f69e787a0339122a804dd518a007/mtp/mtp.go#L94) |
| go-mtpfs session ID | 每次选择非零、非 `0xffffffff` 的随机 session ID。 | 是它自身的实现选择;没有证明随机化能解决 OpenHarmony 的状态未清除。[实现](https://github.com/ganeshrvel/go-mtpfs/blob/1c3302b3c476f69e787a0339122a804dd518a007/mtp/ops.go#L16) |
| Android File Transfer for Linux/macOS(whoozle) | Session Close 发 CloseSession 并等响应;通用 Device Open 有可选 reset,但其 Darwin backend 的 `Device::Reset()` 是空实现。 | 不能从工具支持 macOS 或方法名推断它进行了有效 USB reset。这是开源独立项目,非 Google 的同名 Android File Transfer。[项目](https://github.com/whoozle/android-file-transfer-linux)、[session](https://github.com/whoozle/android-file-transfer-linux/blob/88926930db41238c7b4d7237fc5849b9586cc7b8/mtp/ptp/Session.cpp#L105)、[Darwin 空实现](https://github.com/whoozle/android-file-transfer-linux/blob/88926930db41238c7b4d7237fc5849b9586cc7b8/mtp/backend/darwin/usb/Device.cpp#L38) |
本次未取得 Google 闭源 Android File Transfer 的第一方 host 源码,因此不声称其关闭顺序或私有恢复方式。OpenMTP 的 Huawei P30 Pro [公开问题 #452](https://github.com/ganeshrvel/openmtp/issues/452) 报告的是目录/传输故障,也不能作为 HarmonyOS 当前设备的重连成功证明。
## 按实际设备能力开放功能
建立会话后读取设备 `OperationsSupported`、`DevicePropertiesSupported`,对象功能还要结合格式、属性描述、存储写权限和操作实际返回值。广告支持不等于本次操作成功,也不等于所有对象类型都可用。OpenHarmony 所查代码声明了 delete、move、copy、thumbnail、battery 和对象属性,但实际商业设备返回值才是应用开关依据。[OpenHarmony DeviceInfo 声明](https://github.com/openharmony/multimedia_media_library/blob/4a411de74cfaf694c14820a51a0e900b04241fe6/services/media_mtp/src/payload_data/get_device_info_data.cpp#L32)
以下为 libmtp 1.1.23 的公开 API 映射;操作码由其 PTP 定义核对。[公开头文件](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.h.in)、[PTP 操作定义](https://github.com/libmtp/libmtp/blob/v1.1.23/src/ptp.h)
| 功能 | 公开 API | 探测与实现边界 |
| --- | --- | --- |
| 改名 | `LIBMTP_Set_File_Name`、`LIBMTP_Set_Folder_Name` | 按实际格式查询 `ObjectFileName(0xdc07)`;要求 `GetObjectPropDesc(0x9802)` 的 GetSet 为可写,并有可用的 `SetObjectPropValue(0x9804)` 或 SetObjectPropList。库的改名 helper 自己检查 GetSet。不要新用已弃用的 `LIBMTP_Set_Object_Filename`。 |
| 删除 | `LIBMTP_Delete_Object` | `DeleteObject(0x100b)`;仍处理 ObjectWriteProtected、AccessDenied 等对象级拒绝。目录递归由应用计划与执行,库文档不保证删除目录会删除全部子对象。 |
| 移动 | `LIBMTP_Check_Capability(...MoveObject)`、`LIBMTP_Move_Object` | `MoveObject(0x1019)`;目标根 parent 参数是 `0`。目录和跨存储行为须验证。标准命令无字节进度,不显示虚假百分比。 |
| 设备内复制 | `LIBMTP_Check_Capability(...CopyObject)`、`LIBMTP_Copy_Object` | `CopyObject(0x101a)`;无字节进度;API 仅返回成功/失败,不返回新对象 ID,完成后重新枚举目标目录。不支持时不可把移动偷偷变成复制后删除。 |
| 缩略图 | `LIBMTP_Get_Thumbnail` | `GetThumb(0x100a)`,并按对象是否有缩略图处理。失败显示普通图标;它不是整图预览保证。RepresentativeSample 是另一组属性/API。 |
| 电量 | `LIBMTP_Get_Batterylevel` | `BatteryLevel(0x5001)` 在设备属性列表中;API还检查 broken battery quirk。只有调用成功且最大值有效时换算,失败/不支持显示未知,不能显示 0%。 |
| 对象属性读取 | `LIBMTP_Is_Property_Supported`、`LIBMTP_Get_String_From_Object`、`LIBMTP_Get_u64/u32/u16/u8_From_Object` | `GetObjectPropsSupported(0x9801)` 按格式,`GetObjectPropDesc(0x9802)`、`GetObjectPropValue(0x9803)` 按具体属性。`Is_Property_Supported` 返回正数/0/负数分别为支持/不支持/查询失败,不能把 -1 当成 true。 |
| 对象属性写入 | `LIBMTP_Set_Object_String/u32/u16/u8`、`LIBMTP_Get_Allowed_Property_Values` | 存在不等于可写;遵循 GetSet、数据类型、枚举/范围。后一个 API 返回合法值,不直接暴露 GetSet;不能据它推断写权限。 |
| 目录下载 | `LIBMTP_Get_Files_And_Folders` + `LIBMTP_Get_File_To_File_Descriptor` | 主机遍历对象树,使用各 storage ID/parent ID;不是单一“递归传输”操作。按实际项汇总大小、进度和失败。 |
| 目录上传 | `LIBMTP_Create_Folder` + `LIBMTP_Send_File_From_File_Descriptor` | 以 association/folder 对象建立层级,再逐文件传输;基础操作为 SendObjectInfo/SendObject 或设备支持的 SendObjectPropList 路径。保留空目录、逐项处理重名和失败。 |
公开 `LIBMTP_Check_Capability` 仅覆盖 GetPartialObject、SendPartialObject、EditObjects、MoveObject、CopyObject,不能拿它询问任意操作码。若前端需要全部 OperationsSupported,应在与固定 libmtp 一起编译的 C 桥接层提供只读投影,或给依赖增加受控 API,不从 UI 解析 `mtp-detect` 文本。[能力实现](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L4132)
GetObjectPropList 也要遵循 libmtp 的已知兼容标志,不能为了“一次取全树”强制启用。对于当前 ID,上游已有 BROKEN_MTPGETOBJPROPLIST/BROKEN_SET_OBJECT_PROPLIST/BROKEN_SEND_OBJECT_PROPLIST;这些是库的兼容策略,不表示设备完全不能改名或传输。[兼容标志](https://github.com/libmtp/libmtp/blob/v1.1.23/src/device-flags.h#L320)
递归删除宜先枚举用户选中的子树,再从叶到根执行。复制/移动目录时,不能仅凭标准操作支持就承诺任意层级、跨存储、同名覆盖或取消的原子性。libmtp 文档明确警告目录操作语义与耗时;应用级递归必须保留部分成功状态,不能自动删除原文件作为未说明的降级。[删除/移动/复制契约](https://github.com/libmtp/libmtp/blob/v1.1.23/src/libmtp.c#L6922)
## 最小验证路线
以下是建议给主任务执行的实验,不是本文已经执行的验证。
1. 串行记录一次连接、只读目录、断开、重连全过程。区分 CloseSession 写请求、等响应、release_interface、USB reset、USB close 的耗时与返回码;确认旧后端进程/线程是否退出、旧 handle 是否完成释放。不要把设备名可枚举当成 MTP 会话已恢复。
2. 将关闭独立于正常传输超时:CloseSession 超时后继续清理,避免反复调用它;不改变大文件传输的合理超时。先保留当前 release→reset 顺序,检查这次 reset 是否真的完成。若产品需要状态回传,在固定库边界内显式提供结果。
3. 旧 handle 全部关闭后,按新枚举结果重连,实际完成 GetStorageIDs 与至少一次目录读取。只观察 UI 按钮恢复、扫描返回设备、reset 返回 0 均不足以通过。
4. 只在基线有完整日志后比较候选方案:释放接口后 reset;必要时用全新 raw USB handle 做独立恢复。一次实验只变一处,避免短超时、随机 ID、不同 reset 顺序、额外库补丁同时变化而无法定位。
5. 若 reset 返回 NOT_FOUND,按 API 重新发现设备;若 reset 无响应或设备不重新出现,显示恢复失败和真实状态,不无限循环。硬件重新插拔能恢复,不足以证明软件重枚举一定具备等价效果。
6. 修复通过后,在同一 USB 插接周期连续完成多次 connect→browse→disconnect→connect,并包含插件关闭/退出再打开。写操作能力在专用测试目录验证:改名、删除、移动、复制、中文/emoji、空文件、空目录、嵌套传输,下载回传比对哈希;缩略图和电量失败应保留其余浏览能力。
本研究实际完成:读取项目固定依赖源码;在线查询并固定第一方仓库提交;核对 OpenHarmony 会话调用链、libmtp API、libusb Darwin reset、OpenMTP 与 whoozle 实现;查询相关公开 issue/PR 状态。未修改实现、未打开或重置 USB、未把开源静态分析标为商业设备实测根因。