fix(dev): ensure_pg 智能定位 Docker Desktop(装 D 盘等非默认盘符也能自动拉起)
原先把 Docker Desktop.exe 写死在 %ProgramFiles%\Docker\Docker\,装在 D 盘就自动拉起 失败(明明 docker CLI 已在 PATH 上可用、只是 daemon 没启)。改为按可靠性排序的多路探测: DOCKER_DESKTOP_EXE 显式指定 → 从 PATH 上 docker CLI 反推安装目录(跟随实际盘符)→ 注册表 → 各 Program Files 变体兜底;找不到时提示 DOCKER_DESKTOP_EXE / sqlite 逃生舱。 .env.example 记一笔用法。 顺带修一个先前就红、与现行"sqlite→降级返 True"契约矛盾的测试 (test_ensure_rejects_sqlite → test_ensure_sqlite_escape_hatch)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+89
-12
@@ -16,11 +16,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -79,10 +81,83 @@ def _port_open(host: str, port: int, timeout: float = 1.0) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _docker_desktop_cmd(platform: str, program_files: str) -> list[str] | None:
|
||||
"""按平台给出启动 Docker Desktop 的命令;Linux 返回 None(daemon 需 sudo,让用户手动)。"""
|
||||
def _win_docker_desktop_candidates(
|
||||
env: Mapping[str, str], docker_cli: str | None
|
||||
) -> list[PureWindowsPath]:
|
||||
"""Windows 上 Docker Desktop.exe 的候选路径(按优先级)。纯函数:不碰文件系统、不读注册表。
|
||||
|
||||
用 PureWindowsPath 解析,故在任意 OS 上跑单测都按 Windows 语义(反斜杠分隔),行为确定。
|
||||
优先级:
|
||||
① 环境变量 DOCKER_DESKTOP_EXE 显式指定(终极逃生舱,盘符随你);
|
||||
② 从 PATH 上的 docker CLI 反推——Docker Desktop 的 CLI 在
|
||||
<安装目录>\\resources\\bin\\docker.exe,往上几级即安装目录,天然跟随实际盘符
|
||||
(装在 D 盘就反推出 D 盘,不再写死 C 盘);多取几级容忍未来目录布局微调;
|
||||
③ 各 Program Files 变体下的标准安装路径兜底(覆盖常规 C 盘装)。
|
||||
调用方按序取第一个真实存在的。
|
||||
"""
|
||||
out: list[PureWindowsPath] = []
|
||||
override = (env.get("DOCKER_DESKTOP_EXE") or "").strip().strip('"')
|
||||
if override:
|
||||
out.append(PureWindowsPath(override))
|
||||
if docker_cli:
|
||||
for parent in list(PureWindowsPath(docker_cli).parents)[:4]:
|
||||
out.append(parent / "Docker Desktop.exe")
|
||||
for var in ("ProgramFiles", "ProgramW6432", "ProgramFiles(x86)"):
|
||||
root = env.get(var)
|
||||
if root:
|
||||
out.append(PureWindowsPath(root) / "Docker" / "Docker" / "Docker Desktop.exe")
|
||||
return out
|
||||
|
||||
|
||||
def _docker_desktop_from_registry() -> Path | None:
|
||||
"""从注册表尽力取 Docker Desktop.exe 位置(best-effort;非 Windows / 任何异常都当没找到)。
|
||||
|
||||
比路径猜测更权威且完全跟随实际盘符。探两处:
|
||||
- App Paths\\Docker Desktop.exe 的默认值(通常就是 exe 全路径);
|
||||
- Uninstall\\Docker Desktop 的 InstallLocation(安装目录,需再拼 exe 名)。
|
||||
"""
|
||||
try:
|
||||
import winreg
|
||||
except ImportError: # 非 Windows
|
||||
return None
|
||||
probes = (
|
||||
(winreg.HKEY_LOCAL_MACHINE,
|
||||
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Docker Desktop.exe", "", False),
|
||||
(winreg.HKEY_LOCAL_MACHINE,
|
||||
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Docker Desktop",
|
||||
"InstallLocation", True),
|
||||
)
|
||||
for hive, subkey, value_name, join_exe in probes:
|
||||
try:
|
||||
with winreg.OpenKey(hive, subkey) as key:
|
||||
val, _ = winreg.QueryValueEx(key, value_name)
|
||||
except OSError:
|
||||
continue # 键不存在/无权限 → 下一个
|
||||
if not val:
|
||||
continue
|
||||
exe = Path(val) / "Docker Desktop.exe" if join_exe else Path(val)
|
||||
if exe.exists():
|
||||
return exe
|
||||
return None
|
||||
|
||||
|
||||
def _find_docker_desktop_exe() -> Path | None:
|
||||
"""Windows 上尽力定位【真实存在】的 Docker Desktop.exe;遍历候选 + 注册表兜底,找不到返回 None。"""
|
||||
for cand in _win_docker_desktop_candidates(os.environ, shutil.which("docker")):
|
||||
if Path(cand).exists():
|
||||
return Path(cand)
|
||||
return _docker_desktop_from_registry()
|
||||
|
||||
|
||||
def _docker_desktop_cmd(platform: str) -> list[str] | None:
|
||||
"""按平台给出启动 Docker Desktop 的命令。
|
||||
|
||||
Windows:智能定位 exe(见 _find_docker_desktop_exe),找不到 → None。
|
||||
macOS:交给 `open -a Docker`。Linux:None(daemon 需 sudo,让用户手动)。
|
||||
"""
|
||||
if platform.startswith("win"):
|
||||
return [str(Path(program_files) / "Docker" / "Docker" / "Docker Desktop.exe")]
|
||||
exe = _find_docker_desktop_exe()
|
||||
return [str(exe)] if exe else None
|
||||
if platform == "darwin":
|
||||
return ["open", "-a", "Docker"]
|
||||
return None
|
||||
@@ -119,16 +194,18 @@ def _start_docker_daemon() -> bool:
|
||||
"""守护进程没起时按平台拉起,轮询到就绪。返回是否成功。"""
|
||||
if _docker_daemon_ok():
|
||||
return True
|
||||
cmd = _docker_desktop_cmd(
|
||||
sys.platform, os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||
)
|
||||
cmd = _docker_desktop_cmd(sys.platform)
|
||||
if cmd is None:
|
||||
_log("Docker 守护进程未运行。Linux 请手动:sudo systemctl start docker,然后重试。")
|
||||
if sys.platform.startswith("win"):
|
||||
# docker CLI 在 PATH 上(否则走不到这)、却定位不到 Docker Desktop.exe:多为非标准安装位置
|
||||
_log("找不到 Docker Desktop.exe(已试:PATH 上 docker CLI 反推、注册表、常见安装目录)。")
|
||||
_log(" 确已安装 → 设环境变量 DOCKER_DESKTOP_EXE=<Docker Desktop.exe 全路径> 再重试,"
|
||||
"或先手动启动 Docker Desktop。")
|
||||
_log(f" 不想折腾 → 把 .env 的 DATABASE_URL 改成 {SQLITE_URL} 可降级用 SQLite 跑(仅救急)。")
|
||||
else:
|
||||
_log("Docker 守护进程未运行。Linux 请手动:sudo systemctl start docker,然后重试。")
|
||||
return False
|
||||
if sys.platform.startswith("win") and not Path(cmd[0]).exists():
|
||||
_log(f"找不到 Docker Desktop:{cmd[0]}。请手动启动 Docker Desktop 后重试。")
|
||||
return False
|
||||
_log("启动 Docker Desktop(首次冷启可能 30-60s)…")
|
||||
_log(f"启动 Docker Desktop(首次冷启可能 30-60s):{cmd[0]}")
|
||||
try:
|
||||
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except OSError as e:
|
||||
|
||||
Reference in New Issue
Block a user