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

+94 -13
View File
@@ -161,6 +161,28 @@ def start_service() -> tuple[bool, str]:
return False, output.strip() or f"sc start 返回 {code}"
def restart_service() -> tuple[bool, str]:
"""重启 Apple 服务(先停再起)。
用于「服务装着、也在跑,但 27015 协议往返失败」的场景 —— 通常是服务卡死。
比重新安装(几十到几百 MB 下载)便宜得多,应该优先尝试。
"""
if not plat.IS_WINDOWS:
return False, "非 Windows 平台"
_run(["sc", "stop", SERVICE_NAME], timeout=90)
# 等它真的停下来(服务停止是异步的,立刻 start 可能报 1056/1052)
deadline = time.time() + 25
while time.time() < deadline:
code, output = _run(["sc", "query", SERVICE_NAME], timeout=15)
if "STOPPED" in output.upper() or "1060" in output:
break
time.sleep(1.0)
code, output = _run(["sc", "start", SERVICE_NAME], timeout=90)
if code == 0 or "1056" in output:
return True, "服务已重启"
return False, output.strip() or f"sc start 返回 {code}"
def usbmux_probe(
message_type: str = "ReadBUID",
host: str = USBMUX_HOST,
@@ -650,6 +672,29 @@ def ensure(
return {"ok": True, "applicable": True, "action": "start_service",
"status": after_start}
# 服务存在(甚至在运行)但 27015 探针不通 —— 多半是服务卡死。
# **先重启服务**,不要一上来就走安装阶梯:AMDS 明明已经装好,
# 却去重新执行 38MB MSI、再下载 193MB 的 iTunes,完全是浪费且可能装坏。
if before["service"]["exists"]:
emit("warn", "Apple 服务已安装但 usbmuxd(27015) 不通,先尝试重启服务…")
for attempt in (1, 2):
ok, message = restart_service()
emit("info" if ok else "warn", f"重启服务(第 {attempt} 次):{message}")
time.sleep(2.0 * attempt)
after_restart = status(deep=True)
if after_restart["ready"]:
return {"ok": True, "applicable": True, "action": "restart_service",
"status": after_restart}
if not allow_install or strategy == "none":
return {"ok": False, "applicable": True, "action": "restart_service",
"status": after_restart,
"error": "Apple 服务已安装但 usbmuxd 仍不可用(已尝试重启服务)。"
"请检查是否有防火墙/安全软件拦截 127.0.0.1:27015,"
"或断开重连数据线后重试。"}
# 服务存在但重启无效:仅在用户显式要求时才强制重装,避免无谓的大体积下载
emit("warn", "重启服务仍无效。若确认驱动损坏,可手动强制重装 iTunes:"
"https://www.apple.com/itunes/download/win64")
if not allow_install or strategy == "none":
return {"ok": False, "applicable": True, "action": "none",
"needs_install": True, "status": before,
@@ -677,12 +722,9 @@ def ensure(
result = function(log)
attempts.append({"strategy": name, **{k: v for k, v in result.items()
if k not in ("steps",)}})
if result.get("ok"):
if name == "msi" and not result.get("ok"):
continue
# 无论成功失败都让服务有机会起来,然后复检
# 装成功(或要求重启)时,给服务一次起来的机会再复检;
# 失败则直接落到下一个策略(msi → itunes → winget 逐级降级)
if result.get("ok") or result.get("reboot_required"):
service_state()
start_service()
time.sleep(2.0)
after = status(deep=True)
@@ -729,15 +771,32 @@ DDI_FILES = {
DDI_SUBDIR = ("resources", "ddi", "personalized")
def ddi_dir(root: Path | str | None = None) -> Path:
"""返回开发者镜像的本地目录。"""
base = Path(root) if root else plat.resolve_roots()[0]
return base.joinpath(*DDI_SUBDIR)
def ddi_candidates(root: Path | str | None = None) -> list[Path]:
"""按优先级返回镜像**可能所在**的目录。
打包后资源在 ``sys._MEIPASS``(PyInstaller 解包的临时目录,只读、退出即删),
所以下载得到的镜像不能放那里;用户目录 ``~/.ios-location-ddi`` 是第二候选,
``core/server.py:_stage_personalized_ddi`` 的搜索列表里已经包含它。
"""
if root:
return [Path(root).joinpath(*DDI_SUBDIR)]
resources = plat.resolve_roots()[0]
return [
resources.joinpath(*DDI_SUBDIR), # 随包内置(只读)
Path.home() / ".ios-location-ddi" / "personalized", # 运行期下载(可写)
]
def ddi_status(root: Path | str | None = None) -> dict[str, Any]:
"""检查本地开发者镜像素材是否齐全。"""
directory = ddi_dir(root)
def ddi_write_dir(root: Path | str | None = None) -> Path:
"""返回**可写**的镜像目录(下载目标)。"""
if root:
return Path(root).joinpath(*DDI_SUBDIR)
if getattr(sys, "frozen", False):
return Path.home() / ".ios-location-ddi" / "personalized"
return plat.resolve_roots()[0].joinpath(*DDI_SUBDIR)
def _ddi_status_one(directory: Path) -> dict[str, Any]:
files: dict[str, Any] = {}
missing: list[str] = []
total = 0
@@ -762,6 +821,27 @@ def ddi_status(root: Path | str | None = None) -> dict[str, Any]:
}
def ddi_status(root: Path | str | None = None) -> dict[str, Any]:
"""检查本地开发者镜像素材是否齐全(按候选目录优先级,返回第一个齐全的)。"""
candidates = ddi_candidates(root)
for candidate in candidates:
info = _ddi_status_one(candidate)
if info["ok"]:
return info
# 都不齐全:报告优先级最高的那个(通常就是随包内置的),便于用户理解缺什么
info = _ddi_status_one(candidates[0])
info["searched"] = [str(c) for c in candidates]
return info
def ddi_dir(root: Path | str | None = None) -> Path:
"""返回镜像实际所在目录:优先齐全的那个,否则返回可写目录。"""
for candidate in ddi_candidates(root):
if (candidate / "Image.dmg").exists():
return candidate
return ddi_write_dir(root)
def _ddi_urls(name: str) -> list[str]:
path = f"{DDI_DIR}/{name}"
return [
@@ -784,7 +864,7 @@ def download_developer_image(
if log:
log(level, message)
directory = ddi_dir(root)
directory = ddi_write_dir(root)
try:
directory.mkdir(parents=True, exist_ok=True)
except OSError as exc:
@@ -794,6 +874,7 @@ def download_developer_image(
if current["ok"] and not force:
return {"ok": True, "action": "none", "status": current,
"detail": f"已就绪({current['total_mb']} MB)"}
emit("info", f"镜像下载目录:{directory}")
for name, minimum in DDI_FILES.items():
target = directory / name