feat: Windows v1.1.1 — 修复与改进(DSH 工作区归档)
This commit is contained in:
1 parent
4a5d676c7b
commit
e6880efdc6
21 files changed
+807
-106
No files matched your search
+83
-23
@@ -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/"):
|
||||
|
||||
Reference in new issue
Block a user