feat: Windows v1.1.1 — 修复与改进(DSH 工作区归档)

This commit is contained in:
jbjf6666 committed 2026-10-29 15:37:17 +08:00
1 parent 4a5d676c7b
commit e6880efdc6
21 files changed
+807 -106

No files matched your search

+83 -23
View File
@@ -136,11 +136,26 @@ class App:
# ---------------- 日志 ----------------
def _open_log_file(self):
"""打包成窗口程序后没有控制台,日志必须落盘,否则出问题无从排查。"""
"""打包成窗口程序后没有控制台,日志必须落盘,否则出问题无从排查。
带简单滚动:超过 5 MB 就把当前日志改名为 ``app.log.1``(只保留一份旧日志),
避免长期使用后日志无限增长。
"""
try:
directory = self.root / "logs"
directory.mkdir(parents=True, exist_ok=True)
handle = open(directory / "app.log", "a", encoding="utf-8")
log_path = directory / "app.log"
try:
if log_path.exists() and log_path.stat().st_size > 5 * 1024 * 1024:
backup = directory / "app.log.1"
try:
backup.unlink()
except OSError:
pass
log_path.replace(backup)
except OSError:
pass
handle = open(log_path, "a", encoding="utf-8")
return handle
except Exception:
return None
@@ -252,14 +267,17 @@ class App:
rsd = None
rsd_obj = None
if (device.ios_major or 0) >= 17:
if not self.tunnel.ready:
snapshot = self.tunnel.ensure(device.udid, allow_sudo=False)
if snapshot["state"] != "connected":
raise protocol.AppError(
snapshot.get("message") or tunnel_mod._HINT_NATIVE_FAILED,
"tunnel_required",
status=409,
)
# 必须**无条件**调用 ensure():它内部会判断已建立的隧道是不是绑在当前
# 选中的这台设备上,UDID 不一致会自动重建。
# 之前这里先用 `if not self.tunnel.ready` 短路,导致切换设备时复用了
# 上一台设备的 RSD 对象 —— 表现为"给错的手机下发定位"。
snapshot = self.tunnel.ensure(device.udid, allow_sudo=False)
if snapshot["state"] != "connected":
raise protocol.AppError(
snapshot.get("message") or tunnel_mod._HINT_NATIVE_FAILED,
"tunnel_required",
status=409,
)
rsd_obj = self.tunnel.rsd_object()
if rsd_obj is None:
rsd = self.tunnel.address()
@@ -270,7 +288,21 @@ class App:
udid=device.udid,
log=self.log,
)
self.runner.run(provider.connect(device), timeout=40.0)
try:
self.runner.run(provider.connect(device), timeout=40.0)
except Exception:
# 失败时把新建的 provider 关掉,避免 DVT/lockdown 泄漏
try:
self.runner.run(provider.close(), timeout=15.0)
except Exception:
pass
# 并把隧道标记为失效,下一次连接会重建,
# 避免拿着一个已经死掉的 RSD 反复重试。
try:
self.tunnel.invalidate("DVT 会话建立失败,隧道可能已失效")
except Exception:
pass
raise
self.provider = provider
self.provider_udid = device.udid
self.playback.set_provider(provider)
@@ -810,6 +842,8 @@ class App:
"""
import sys
from . import apple_driver
sources: list[Path] = []
base = getattr(sys, "_MEIPASS", None)
if base:
@@ -819,8 +853,15 @@ class App:
sources.append(self.root / "resources" / "ddi" / "personalized")
sources.append(Path.home() / ".ios-location-ddi" / "personalized")
src = next((d for d in sources if (d / "Image.dmg").exists()), None)
# 必须**三件齐全**的目录才算可用。以前只判断"有没有 Image.dmg",
# 可能挑到一个残缺目录(比如缺 Image.dmg.trustcache),拷进去照样挂不上 ——
# 与 apple_driver.ddi_status() 的判定标准保持一致。
required = tuple(apple_driver.DDI_FILES)
src = next((d for d in sources
if all((d / name).exists() for name in required)), None)
if src is None:
self.log("warn", "未找到完整的个性化镜像素材(需要 Image.dmg / "
"BuildManifest.plist / Image.dmg.trustcache 三件齐全)")
return False
dest = Path.home() / ".pymobiledevice3" / "Xcode_iOS_DDI_Personalized"
@@ -834,8 +875,6 @@ class App:
copied = []
for source_name, dest_name in mapping.items():
source_file = src / source_name
if not source_file.exists():
continue
target = dest / dest_name
# 体积一致就跳过,避免每次挂载都拷 15MB
if target.exists() and target.stat().st_size == source_file.stat().st_size:
@@ -844,18 +883,15 @@ class App:
copied.append(dest_name)
if copied:
self.log("info", "已从应用内置素材安装个性化镜像:" + "、".join(copied))
# 落地后再校验一次,避免"拷了但没拷全"
missing = [d for d in mapping.values() if not (dest / d).exists()]
if missing:
self.log("warn", "镜像素材不完整,缺少:" + "、".join(missing))
return False
return True
except Exception as exc:
self.log("warn", f"安装内置个性化镜像失败:{exc}")
return False
device = self.devices.get_selected()
if device is None:
return {"mounted": None, "running": self._mount_running}
return {
"mounted": self._developer_image_mounted(device.udid),
"running": self._mount_running,
"ios_major": device.ios_major,
}
def _cmd_mount_status(self, message: dict[str, Any]) -> dict[str, Any]:
"""查询当前设备的开发者镜像/会话状态。"""
@@ -984,6 +1020,14 @@ class App:
self.runner.shutdown()
except Exception:
pass
# 关闭日志文件句柄(打包成窗口程序时这是唯一的排查线索,必须 flush 落盘)
try:
handle = getattr(self, "_log_file", None)
if handle is not None:
handle.close()
self._log_file = None
except Exception:
pass
# ---------------- HTTP ----------------
@@ -1001,6 +1045,9 @@ class Handler(BaseHTTPRequestHandler):
def handle_one_request(self) -> None:
"""浏览器关闭 keep-alive 连接时不要往终端刷一堆 ConnectionResetError 回溯。"""
# 同一个 handler 实例会处理 keep-alive 连接上的多个请求,
# 请求体缓存必须按请求重置,否则第二个请求会读到上一个请求的 body。
self._body_cache = None
try:
super().handle_one_request()
except (ConnectionResetError, BrokenPipeError, ConnectionAbortedError):
@@ -1070,15 +1117,22 @@ class Handler(BaseHTTPRequestHandler):
pass
def _read_body(self) -> bytes:
# do_POST 会预先读完并缓存,这里直接复用,避免重复读取或漏读
cached = getattr(self, "_body_cache", None)
if cached is not None:
return cached
try:
length = int(self.headers.get("Content-Length") or 0)
except ValueError:
length = 0
if length <= 0:
self._body_cache = b""
return b""
if length > 64 * 1024 * 1024:
raise protocol.AppError("请求体过大", "too_large", status=413)
return self.rfile.read(length)
data = self.rfile.read(length)
self._body_cache = data
return data
def _json_body(self) -> dict[str, Any]:
raw = self._read_body()
@@ -1120,6 +1174,12 @@ class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802
path = urllib.parse.urlparse(self.path).path
try:
# **先把请求体读完并缓存**。有些 handler(如 /api/devices/connect)
# 压根不读 body;若不在这里排空,残留的字节会留在 keep-alive 连接里,
# 被当成下一个请求的请求行 —— 表现为
# 501 Unsupported method ('{}POST')
# 而且是间歇性的(取决于客户端是否复用连接),很难查。
self._body_cache = self._read_body()
self._check_origin()
self._check_host()
if path.startswith("/api/"):