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
+122
-26
@@ -20,6 +20,7 @@ from typing import Any, Callable
|
||||
|
||||
from . import apple_driver
|
||||
from . import platform as plat
|
||||
from . import protocol
|
||||
|
||||
# 颜色(与 web 端保持同一套语义色)
|
||||
COLOR_OK = "#1a7f37"
|
||||
@@ -144,7 +145,23 @@ def _install_developer_image(log: Callable[[str, str], None]) -> tuple[bool, str
|
||||
def _check_permission() -> tuple[bool, str]:
|
||||
if plat.is_admin():
|
||||
return True, "已以管理员身份运行"
|
||||
return False, "未提权(安装 Apple 驱动和管理员操作会失败)"
|
||||
return False, "未提权(安装 Apple 驱动会失败)"
|
||||
|
||||
|
||||
def _install_permission(log: Callable[[str, str], None]) -> tuple[bool, str]:
|
||||
"""「安装」管理员权限 = 以管理员身份重新启动本程序。
|
||||
|
||||
之前这一项没有安装动作,导致**非管理员用户永远无法点「进入程序」**(死锁)。
|
||||
现在点这个按钮会通过 UAC 拉起一个提权的新实例,本实例随即退出。
|
||||
"""
|
||||
if plat.is_admin():
|
||||
return True, "已经是管理员身份"
|
||||
log("info", "正在通过 UAC 以管理员身份重新启动程序…")
|
||||
launched = plat.relaunch_elevated()
|
||||
if launched:
|
||||
log("info", "已发起提权重启;请在系统弹窗里点「是」")
|
||||
return True, "已发起提权重启,本窗口即将关闭"
|
||||
return False, "无法自动提权,请关闭程序后右键 →「以管理员身份运行」"
|
||||
|
||||
|
||||
def _check_device() -> tuple[bool, str]:
|
||||
@@ -161,20 +178,29 @@ def _check_device() -> tuple[bool, str]:
|
||||
|
||||
|
||||
def _check_network() -> tuple[bool, str]:
|
||||
"""Apple TSS 签名需要联网(挂载开发者镜像时用)。"""
|
||||
"""检查两个**用途不同**的网络依赖,分开报,不要混为一谈。
|
||||
|
||||
- ``gs.apple.com``:挂载开发者镜像时向 Apple 申请 TSS 签名,**必须**能连。
|
||||
- GitHub / jsDelivr:只在「开发者镜像缺失、需要现下」时用;镜像已内置则无所谓。
|
||||
国内经常只有 GitHub 被墙而 Apple 正常,以前那种"二选一"的判断会给出误导性结论。
|
||||
"""
|
||||
import socket
|
||||
|
||||
hosts = [("gs.apple.com", 443), ("raw.githubusercontent.com", 443)]
|
||||
reachable = []
|
||||
for host, port in hosts:
|
||||
def _reachable(host: str, port: int = 443) -> bool:
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=6):
|
||||
reachable.append(host)
|
||||
return True
|
||||
except OSError:
|
||||
continue
|
||||
if reachable:
|
||||
return True, "可连通(" + "、".join(reachable) + ")"
|
||||
return False, "无法连接 Apple 服务器(挂载开发者镜像时需要联网签名)"
|
||||
return False
|
||||
|
||||
apple_ok = _reachable("gs.apple.com")
|
||||
mirror_ok = _reachable("raw.githubusercontent.com") or _reachable("cdn.jsdelivr.net")
|
||||
|
||||
if apple_ok and mirror_ok:
|
||||
return True, "Apple 签名服务与镜像下载源均可连通"
|
||||
if apple_ok:
|
||||
return True, "Apple 签名服务可连通(镜像源不可达,但镜像已内置,通常无影响)"
|
||||
return False, "无法连接 Apple 签名服务器 gs.apple.com —— 挂载开发者镜像时会失败"
|
||||
|
||||
|
||||
def build_items() -> list[EnvItem]:
|
||||
@@ -204,8 +230,9 @@ def build_items() -> list[EnvItem]:
|
||||
))
|
||||
items.append(EnvItem(
|
||||
"permission", "管理员权限",
|
||||
"安装驱动与系统级操作需要",
|
||||
_check_permission, None, required=True,
|
||||
"安装 Apple 驱动需要;虚拟定位本身不需要",
|
||||
_check_permission, _install_permission, required=True,
|
||||
install_label="重启提权",
|
||||
))
|
||||
items.append(EnvItem(
|
||||
"device", "iPhone 连接",
|
||||
@@ -253,9 +280,34 @@ class SetupWindow:
|
||||
root.resizable(False, True)
|
||||
self._fit_to_screen(root)
|
||||
root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
root.after(80, self._pump)
|
||||
self._pump_after_id: str | None = None
|
||||
self._schedule_pump()
|
||||
self.refresh_all()
|
||||
|
||||
def _schedule_pump(self) -> None:
|
||||
"""排下一次 UI 泵,并记住 after id 以便销毁前取消。"""
|
||||
try:
|
||||
self._pump_after_id = self.root.after(80, self._pump)
|
||||
except Exception:
|
||||
self._pump_after_id = None
|
||||
|
||||
def _cancel_pump(self) -> None:
|
||||
"""销毁窗口前取消待执行的 after,否则 Tk 会打印
|
||||
``invalid command name "..._pump"``(窗口程序看不到,命令行会刷屏)。"""
|
||||
if self._pump_after_id is not None:
|
||||
try:
|
||||
self.root.after_cancel(self._pump_after_id)
|
||||
except Exception:
|
||||
pass
|
||||
self._pump_after_id = None
|
||||
|
||||
def _destroy(self) -> None:
|
||||
self._cancel_pump()
|
||||
try:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ---------------- 布局 ----------------
|
||||
@staticmethod
|
||||
def _fit_to_screen(window: Any, min_width: int = 820, min_height: int = 560) -> None:
|
||||
@@ -286,8 +338,13 @@ class SetupWindow:
|
||||
|
||||
header = tk.Frame(self.root, bg=COLOR_BG)
|
||||
header.pack(fill="x", padx=20, pady=(16, 6))
|
||||
tk.Label(header, text=title, font=("Microsoft YaHei UI", 15, "bold"),
|
||||
bg=COLOR_BG).pack(anchor="w")
|
||||
title_row = tk.Frame(header, bg=COLOR_BG)
|
||||
title_row.pack(fill="x", anchor="w")
|
||||
tk.Label(title_row, text=title, font=("Microsoft YaHei UI", 15, "bold"),
|
||||
bg=COLOR_BG).pack(side="left")
|
||||
tk.Label(title_row, text=f"v{protocol.APP_VERSION} · {plat.platform_label()}",
|
||||
font=("Microsoft YaHei UI", 9), fg=COLOR_MUTED,
|
||||
bg=COLOR_BG).pack(side="left", padx=(10, 0), pady=(4, 0))
|
||||
tk.Label(
|
||||
header,
|
||||
text="首次使用需要准备以下组件。缺失的项目点右侧按钮即可自动安装;"
|
||||
@@ -369,6 +426,20 @@ class SetupWindow:
|
||||
command=self.refresh_all)
|
||||
self.recheck_button.pack(side="right")
|
||||
|
||||
self.open_logs_button = ttk.Button(footer, text="打开日志目录", width=13,
|
||||
command=self._open_logs_dir)
|
||||
self.open_logs_button.pack(side="right", padx=(0, 8))
|
||||
|
||||
def _open_logs_dir(self) -> None:
|
||||
"""打开 %LOCALAPPDATA%\\iOSLocationTrack\\logs —— 装驱动失败时让用户能自己取证。"""
|
||||
try:
|
||||
target = plat.data_dir() / "logs"
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
if not plat.open_in_file_manager(target):
|
||||
self.append_log(f"日志目录:{target}")
|
||||
except Exception as exc:
|
||||
self.append_log(f"无法打开日志目录:{exc}")
|
||||
|
||||
# ---------------- 日志 ----------------
|
||||
def append_log(self, text: str) -> None:
|
||||
self.log.configure(state="normal")
|
||||
@@ -439,10 +510,21 @@ class SetupWindow:
|
||||
except Exception as exc:
|
||||
ok, detail = False, f"安装异常:{exc}"
|
||||
self.events.put(("install", (item.key, ok, detail)))
|
||||
if item.key == "permission" and ok:
|
||||
# 提权重启已发起:关掉本窗口,本进程退出,由新实例接管
|
||||
self.events.put(("close", None))
|
||||
return
|
||||
self.events.put(("installs_done", None))
|
||||
|
||||
# ---------------- UI 线程泵 ----------------
|
||||
def _pump(self) -> None:
|
||||
# 窗口一旦销毁(点「进入程序」/关闭窗口/提权重启),就不要再碰控件:
|
||||
# 否则队列里排着的 after 回调会在 Tk 里报 invalid command name ..._pump。
|
||||
try:
|
||||
if not self.root.winfo_exists():
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
while True:
|
||||
kind, payload = self.events.get_nowait()
|
||||
@@ -450,6 +532,9 @@ class SetupWindow:
|
||||
key, ok, detail = payload
|
||||
self.state[key] = {"ok": ok, "detail": detail, "busy": False}
|
||||
self._render(key)
|
||||
# 每完成一项就刷新底部:否则必需项都绿了,
|
||||
# 还要等可选项(网络探测可能 3×6s / PnP 45s)跑完按钮才亮。
|
||||
self._update_footer()
|
||||
elif kind == "checks_done":
|
||||
self._update_footer()
|
||||
elif kind == "busy":
|
||||
@@ -471,10 +556,16 @@ class SetupWindow:
|
||||
self.refresh_all()
|
||||
elif kind == "log":
|
||||
self.append_log(payload)
|
||||
elif kind == "close":
|
||||
# 提权重启:关掉本窗口。main() 看到 proceeded=False 会正常退出,
|
||||
# 由刚拉起的提权实例继续跑(它自己会再弹一次本窗口,那时已是管理员)。
|
||||
self.proceeded = False
|
||||
self._destroy()
|
||||
return
|
||||
except queue.Empty:
|
||||
pass
|
||||
if not self.proceeded:
|
||||
self.root.after(80, self._pump)
|
||||
self._schedule_pump()
|
||||
|
||||
# ---------------- 渲染 ----------------
|
||||
def _render(self, key: str) -> None:
|
||||
@@ -521,9 +612,20 @@ class SetupWindow:
|
||||
self.state[key]["detail"] = detail
|
||||
self._render(key)
|
||||
|
||||
def _needs_item(self, item: EnvItem) -> bool:
|
||||
"""判断某一项此刻是否真的"必需"。
|
||||
|
||||
「管理员权限」只在**需要安装 Apple 驱动**时才是必需的:驱动已就绪时,
|
||||
建隧道(userspace 免管理员)和下发定位都不需要提权,不该因为它把
|
||||
「进入程序」锁死 —— 否则源码模式 / 非管理员启动就永远进不去。
|
||||
"""
|
||||
if item.key == "permission":
|
||||
driver = self.state.get("apple_driver") or {}
|
||||
return driver.get("ok") is not True
|
||||
return item.required
|
||||
|
||||
def _update_footer(self) -> None:
|
||||
required = [i for i in self.items if i.required]
|
||||
self.state.get("__x__")
|
||||
required = [i for i in self.items if self._needs_item(i)]
|
||||
pending = [i for i in required if self.state[i.key].get("ok") is not True]
|
||||
optional_bad = [i for i in self.items
|
||||
if not i.required and self.state[i.key].get("ok") is False]
|
||||
@@ -554,17 +656,11 @@ class SetupWindow:
|
||||
# ---------------- 结束 ----------------
|
||||
def _on_confirm(self) -> None:
|
||||
self.proceeded = True
|
||||
try:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self._destroy()
|
||||
|
||||
def _on_close(self) -> None:
|
||||
self.proceeded = False
|
||||
try:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
self._destroy()
|
||||
|
||||
|
||||
def run_setup_window(title: str = "环境准备", auto_install_missing: bool = False) -> bool | None:
|
||||
|
||||
Reference in new issue
Block a user