File size: 10,880 Bytes
126cf9c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | """
抖音港版登录页修复 - 核心逻辑 (v3.1 = v3 逻辑 + conn.Session 并发加固)
供 app.py / main.py 调用, 所有输出通过 log 回调返回.
v3.1 相比 v3:
- SSH/ADB 生命周期改由 conn.Session 托管
· 本地端口动态分配 (不再撞 -L 8377)
· SSH keep-alive (云平台空闲断线不再丢任务)
· 硬超时兜底 (默认 HKDY_HARD_TIMEOUT=180s)
· 自动重连 1 次
· 只清自己的 serial, 绝不 adb kill-server
- 对外签名保持不变:
run_restore(raw_input_text, pack_dir, log, progress, stop_event=None)
-> (ok: bool, verify_png_path: str|None, backup_path: str|None)
backup_path 在 v3 下始终为 None.
- 为兼容 hk_gate.py 等老调用者, 保留 Tunnel / Adb / parse_ssh_command /
adb_connect / adb_wait_device / _filter_three_lines / RestoreError 符号.
"""
import hashlib
import os
import sys
import tempfile
import time
_HERE = os.path.dirname(os.path.abspath(__file__))
_LIB = os.path.join(_HERE, "lib")
if os.path.isdir(_LIB) and _LIB not in sys.path:
sys.path.insert(0, _LIB)
import paramiko # noqa: F401 (back-compat re-exports 可能用到)
from conn import (
Session,
parse_ssh_command, # 转发 (hk_gate.py 等老代码可能用)
_run as _conn_run,
_Tunnel,
)
PKG = "com.ss.android.ugc.aweme.mobile"
# v3: 港版预注册 DID 模板 (唯一资源)
TEMPLATE_NAME = "applog_stats.xml"
TEMPLATE_MD5 = "d2a6209411e4d4454359e1ac906ca16c"
TEMPLATE_SIZE = 3621
class RestoreError(RuntimeError):
pass
# ============ 后向兼容 shim (hk_gate.py 直接 import 这几个) ============
# 新代码请用 conn.Session, 不要再新增对下面符号的引用.
class Tunnel(_Tunnel):
"""Legacy shim. 参数签名与 v2 保持一致."""
def __init__(self, host, port, user, password, local_port, rhost, rport):
super().__init__(host, port, user, password, rhost, rport,
preferred_local=local_port)
class Adb:
"""Legacy shim. 直接操作 adb 命令行, 不带自动重连 (老代码依赖它的行为)."""
def __init__(self, port):
self.port = port
self.serial = f"localhost:{port}"
def sh(self, cmd, check=True, timeout=120):
r = _conn_run(["adb", "-s", self.serial, "shell", cmd], timeout=timeout)
if check and r.returncode != 0:
raise RuntimeError(f"adb shell 失败:\n{cmd}\n{r.stderr or r.stdout}")
return r.stdout or ""
def push(self, local, remote, timeout=1800):
r = _conn_run(["adb", "-s", self.serial, "push", local, remote], timeout=timeout)
if r.returncode != 0:
raise RuntimeError(f"adb push 失败: {r.stderr or r.stdout}")
return r.stdout
def pull(self, remote, local, timeout=300):
r = _conn_run(["adb", "-s", self.serial, "pull", remote, local], timeout=timeout)
if r.returncode != 0:
raise RuntimeError(f"adb pull 失败: {r.stderr or r.stdout}")
return r.stdout
def root(self):
_conn_run(["adb", "-s", self.serial, "root"], timeout=20)
def adb_connect(port):
return _conn_run(["adb", "connect", f"localhost:{port}"], timeout=20).stdout.strip()
def adb_wait_device(port, timeout=20):
_conn_run(["adb", "-s", f"localhost:{port}", "wait-for-device"], timeout=timeout)
def _filter_three_lines(raw):
"""Legacy. 返回 (ssh_cmd, password, adb_cmd). Session 用 _parse_input 内部处理."""
lines = [l.strip() for l in (raw or "").splitlines() if l.strip()]
ssh_cmd = next((l for l in lines if l.lstrip().lower().startswith("ssh ")), None)
adb_cmd = next((l for l in lines if l.lstrip().lower().startswith("adb ")), None)
password = next(
(l for l in lines if not l.lstrip().lower().startswith(("ssh ", "adb "))),
None,
)
return ssh_cmd, password, adb_cmd
# ============ 主流程 ============
def _md5(path):
return hashlib.md5(open(path, "rb").read()).hexdigest()
def run_restore(raw_input_text, pack_dir, log, progress, stop_event=None):
"""主流程. 返回 (ok, verify_png, backup_path).
v3 下 backup_path 始终为 None — pm clear 自身即回滚到出厂态."""
def check_stop():
if stop_event is not None and stop_event.is_set():
raise RestoreError("用户已中断")
# [0] 模板校验
if pack_dir and not os.path.isdir(pack_dir):
try: os.makedirs(pack_dir, exist_ok=True)
except Exception: pass
template = os.path.join(pack_dir, TEMPLATE_NAME) if pack_dir else None
if not template or not os.path.exists(template):
raise RestoreError(f"找不到模板文件: {template}\n"
f"请确认 {TEMPLATE_NAME} 存在于 pack_dir.")
sz_local = os.path.getsize(template)
md5_local = _md5(template)
if sz_local != TEMPLATE_SIZE or md5_local != TEMPLATE_MD5:
log(f"⚠ 模板异常: 大小={sz_local} (期望 {TEMPLATE_SIZE}) md5={md5_local}")
log(f" 期望 md5={TEMPLATE_MD5}. 继续尝试, 港版识别可能失败.")
else:
log(f" 模板 OK: {TEMPLATE_NAME} {sz_local} B md5={md5_local[:8]}…")
# [1/6] SSH 隧道 + adb ready (Session 一把搞定, 带 keep-alive / 自动重连 / 硬超时)
log("[1/6] 建立 SSH 隧道 + adb ready (动态端口, keep-alive, 硬超时 180s)")
progress(10)
check_stop()
try:
sess = Session(raw_input_text, log=log) # hard_timeout 读 HKDY_HARD_TIMEOUT 环境变量
except ValueError as e:
raise RestoreError(str(e))
try:
try:
sess.start()
except paramiko.AuthenticationException:
raise RestoreError("SSH 认证失败: 用户名或密码错误")
except Exception as e:
raise RestoreError(f"建立连接失败: {e}")
log(f" serial={sess.serial} (local_port={sess.local_port})")
progress(22)
check_stop()
# [2/6] 设备信息
who = sess.sh("id", check=False).strip()
if "uid=0" not in who:
log(f" ⚠ 当前非 root: {who}")
brand = sess.sh("getprop ro.product.brand").strip()
model = sess.sh("getprop ro.product.model").strip()
sdk = sess.sh("getprop ro.build.version.sdk").strip()
log(f"[2/6] 设备: {brand} {model} Android SDK={sdk}")
progress(32)
if "package:" not in sess.sh(f"pm path {PKG}", check=False):
raise RestoreError(f"目标设备未安装 {PKG}")
# [3/6] pm clear
log(f"[3/6] pm clear {PKG} — 清空抖音数据")
sess.sh(f"am force-stop {PKG}", check=False)
sess.sh(f"pm clear {PKG}")
time.sleep(2)
progress(45)
check_stop()
data_dir = f"/data/data/{PKG}"
for _ in range(5):
if sess.sh(f"[ -d {data_dir} ] && echo yes || echo no",
check=False).strip() == "yes":
break
time.sleep(1)
else:
raise RestoreError(f"pm clear 后 {data_dir} 不存在")
uid_gid = sess.sh(f"stat -c '%u:%g' {data_dir}").strip()
ctx = sess.sh(f"ls -Zd {data_dir}").strip().split()[0]
log(f" UID={uid_gid} SELinux={ctx}")
progress(55)
# [4/6] 写模板
log(f"[4/6] 写入 shared_prefs/{TEMPLATE_NAME}")
remote_tmp = f"/data/local/tmp/_hkdy_{os.getpid()}_{TEMPLATE_NAME}"
sess.push(template, remote_tmp)
out = sess.sh(
f"mkdir -p {data_dir}/shared_prefs && "
f"cp {remote_tmp} {data_dir}/shared_prefs/{TEMPLATE_NAME} && "
f"chown {uid_gid} {data_dir}/shared_prefs && "
f"chown {uid_gid} {data_dir}/shared_prefs/{TEMPLATE_NAME} && "
f"chmod 660 {data_dir}/shared_prefs/{TEMPLATE_NAME} && "
f"chcon {ctx} {data_dir}/shared_prefs && "
f"chcon {ctx} {data_dir}/shared_prefs/{TEMPLATE_NAME} && "
f"rm -f {remote_tmp} && echo WROTE_OK"
)
if "WROTE_OK" not in out:
raise RestoreError(f"写入失败:\n{out}")
actual_sz = sess.sh(
f"stat -c '%s' {data_dir}/shared_prefs/{TEMPLATE_NAME}",
check=False,
).strip()
log(f" 落地大小: {actual_sz} B (期望 {TEMPLATE_SIZE})")
progress(70)
check_stop()
# [5/6] 启动抖音
log("[5/6] 启动抖音")
sess.sh(f"monkey -p {PKG} -c android.intent.category.LAUNCHER 1",
check=False)
for i in range(12):
check_stop()
time.sleep(1)
progress(70 + i * 2)
# [6/6] 截图验证 + 清理
log("[6/6] 截图验证")
sess.sh("screencap -p /sdcard/verify.png", check=False)
verify_png = None
if pack_dir:
# 带 pid 前缀, 并发任务不会互相覆盖
# 截图临时放 /tmp, finally 里统一清 (用户要求: 不持久化截图)
verify_png = os.path.join(
tempfile.gettempdir(),
f"hkdy_verify_{sess.local_port}_{os.getpid()}.png",
)
try:
sess.pull("/sdcard/verify.png", verify_png)
sz_png = os.path.getsize(verify_png) if os.path.exists(verify_png) else 0
log(f" 截图 {sz_png} B (临时 {verify_png}, 函数退出清理)")
except Exception as e:
log(f" ⚠ 截图拉取失败: {e}")
verify_png = None
act = sess.sh("dumpsys window | grep mCurrentFocus | tail -1",
check=False).strip()
log(f" 当前 Activity: {act}")
# 设备端痕迹清理 (不动别人的残留, 只清自己 pid 相关)
sess.sh(
f"rm -f /sdcard/verify.png /data/local/tmp/_hkdy_{os.getpid()}_* "
"2>/dev/null; "
"logcat -c 2>/dev/null; echo CLEAN_OK",
check=False, timeout=15,
)
progress(100)
log("")
log("=== 修复完成 ===")
log("打开抖音点「同意」后进入登录页, 可见 Google / 邮箱登录按钮.")
log("注意: 所有设备共享同一 HK DID (3590369038608599), 勿在一台上批量登号.")
return True, None, None # 截图不持久化, 不返回路径
finally:
# 清截图
try:
if verify_png and os.path.exists(verify_png):
os.unlink(verify_png)
except Exception:
pass
try:
sess.close()
except Exception:
pass
|