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:
guke
2026-07-28 14:13:11 +08:00
parent c9ef4464f6
commit 3f449d3600
3 changed files with 147 additions and 22 deletions
+89 -12
View File
@@ -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: