From 615f9cf0d598967daa8fa0b16e20f278a736316c Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Mon, 14 Sep 2026 09:08:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(APK=20=E5=AE=89=E8=A3=85):=20=E3=80=8C?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E4=B8=8D=E5=AE=8C=E6=95=B4=E3=80=8D=E6=98=AF?= =?UTF-8?q?=E8=AF=AF=E6=8A=A5=E2=80=94=E2=80=94adb=20=E9=80=80=E5=87=BA=20?= =?UTF-8?q?=E2=89=A0=20=E8=AE=BE=E5=A4=87=E5=86=99=E5=AE=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户报:安装 APK 显示「推送不完整(设备 3605447 / 本地 5933150 字节)」。 排查(220 现场 + 复现):这不是传输失败,是**检查时机太早**。 `adb exec-in` 把数据交给设备侧(adbd → shell → `cat >`)后,缓冲里的数据还在继续落盘: 同一份 5.9MB 的 APK,adb 刚返回时 stat 读到 3710967,紧接着 4264448,几秒后才是完整的 5933150(逐步实测)。旧实现只量一次就判失败,于是同一份文件在多台设备上"失败"了 8 次, 断点各不相同(2.7M/3.3M/3.6M/3.9M)—— 典型误报指纹。 修法(core/apk_manager.py): - 新增 `_wait_remote_size()`:收尾**轮询**设备文件大小直到长齐(默认最长 20s); stat 取不到(机型差异)时返回 None,照旧跳过校验,不误报。 - 只有真不齐才退回 `adb push` 重传(原来直接判失败)。 - `_push_fallback` 也补一次大小校验:adb push 虽同步,但设备空间不足/被并发覆盖时 "1 file pushed" 也可能是残缺的。 影响:这个误报不只是显示难看——它会让整台设备的安装直接失败(明明是好的包)。 --- core/apk_manager.py | 46 +++++++++++++++++++++++++++++++++++++++------ doc/API.md | 6 ++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/core/apk_manager.py b/core/apk_manager.py index d2b2def..037c95f 100644 --- a/core/apk_manager.py +++ b/core/apk_manager.py @@ -539,18 +539,46 @@ class ApkManager: _log.warning(f"[{serial}] exec-in 推送到一半失败(rc={rc}),退回 adb push") return self._push_fallback(serial, apk_path, remote) - # 校验设备上文件大小一致(exec-in 中途断流可能"成功"但少字节) + # 校验设备上文件大小。**必须轮询等它长齐**——见 _wait_remote_size 的说明: + # exec-in 返回后设备侧可能仍在落盘,只量一次会把成功的传输误判成截断。 + got = self._wait_remote_size(serial, remote, total) + if got is not None and got != total: + _log.warning(f"[{serial}] exec-in 推送不完整(设备 {got} / 本地 {total} 字节)," + f"改用 adb push 重传") + return self._push_fallback(serial, apk_path, remote) + + return True, "推送完成 %.1fMB / %.0fs" % (total / 1048576.0, time.time() - t0) + + @staticmethod + def _remote_size(serial, remote): + """设备上文件字节数;取不到返回 None(stat 参数各机型有差异,不据此判失败)。""" try: rs = subprocess.run( [ADB_PATH, "-s", serial, "shell", "stat", "-c", "%s", remote], capture_output=True, timeout=30) - got = int((rs.stdout or b"").decode("utf-8", "replace").strip().splitlines()[-1]) - if got != total: - return False, f"推送不完整(设备 {got} / 本地 {total} 字节)" + lines = (rs.stdout or b"").decode("utf-8", "replace").strip().splitlines() + return int(lines[-1]) if lines else None except Exception: - pass # stat 参数各机型有差异,取不到就跳过校验 + return None - return True, "推送完成 %.1fMB / %.0fs" % (total / 1048576.0, time.time() - t0) + def _wait_remote_size(self, serial, remote, want, timeout=20.0): + """轮询设备上的文件大小,直到等于 want / 超时 / 取不到(None)。 + + 为什么不能只量一次:**adb 进程退出 ≠ 设备上文件写完**。exec-in 把数据交给 + 设备侧(adbd → shell → `cat >`)后,缓冲区里的数据还会继续落盘。实测同一份 + 5.9MB 的 APK:adb 刚返回时 stat 读到 3710967,紧接着 4264448,几秒后才是完整的 + 5933150。旧实现量一次就判"推送不完整",把**成功的传输误报成失败** + (2026-09-14 用户报的就是这个;同一份文件在多台设备上"失败",断点还各不相同)。 + """ + deadline = time.time() + timeout + last = None + while True: + last = self._remote_size(serial, remote) + if last == want or last is None: + return last + if time.time() >= deadline: + return last + time.sleep(0.3) def _push_fallback(self, serial, apk_path, remote): """老 adb 或 exec-in 失败时的退路:原来的 `adb push`(无进度)。""" @@ -565,6 +593,12 @@ class ApkManager: if r.returncode != 0 or "1 file pushed" not in out: return False, (out.strip().replace("\n", " ")[:150] or f"推送失败(returncode={r.returncode})") + # adb push 是同步的(不会出现 exec-in 那种"退出后才落盘"),但仍量一次大小: + # 设备空间不足/文件被并发覆盖时,"1 file pushed" 也可能是残缺的 + total = os.path.getsize(apk_path) + got = self._wait_remote_size(serial, remote, total, timeout=10) + if got is not None and got != total: + return False, f"推送不完整(设备 {got} / 本地 {total} 字节)" return True, "推送完成(adb push)" def _set_item(self, serial, status, msg=""): diff --git a/doc/API.md b/doc/API.md index ab5d8ec..193bfd4 100644 --- a/doc/API.md +++ b/doc/API.md @@ -577,6 +577,12 @@ > 1. **推送到设备**:`推送中 62%(211.0/336.1 MB · 2.6 MB/s · 剩约 48s)` —— 用分块写 > (`adb exec-in`)拿字节级进度;不用 `adb push` 是因为它只在结束时吐一行汇总, > 大包(抖音 336MB≈2 分钟)整个传输过程界面上只能干等 +> ⚠️ **`adb exec-in` 返回 ≠ 设备上写完**:数据还在设备侧(adbd → shell → `cat >`)缓冲里 +> 继续落盘。所以收尾必须**轮询文件大小直到长齐**(`_wait_remote_size`,最长 20s), +> 量一次就判"推送不完整"会把成功的传输误报成失败(2026-09-14 的真实案例:同一份 +> 5.9MB 的 APK 在多台设备上报了 8 次假失败,实际文件最终都是完整的)。真不齐才退回 +> `adb push` 重传。 + > 2. **设备上安装**:`正在设备上安装...(大包要 1-2 分钟,无进度可读)` —— `pm install` > 在设备端解包,系统没给进度接口 >