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
+94
-13
@@ -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
|
||||
|
||||
Reference in new issue
Block a user