fix(install): MySQL TCP probe and clearer host.docker.internal errors
Step 2 now detects whether host MySQL is listening; init-db fails fast with 1Panel setup guidance on error 2003. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+70
-5
@@ -138,6 +138,56 @@ def _probe_redis_installed() -> tuple[bool, str]:
|
||||
return False, f"✗ 未检测到 Redis({hint})"
|
||||
|
||||
|
||||
def _probe_mysql_installed() -> tuple[bool, str]:
|
||||
"""Step 2: TCP probe — MySQL 是否在宿主机监听(不验证账号密码)。"""
|
||||
import socket
|
||||
|
||||
targets: list[tuple[str, int]] = []
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1":
|
||||
targets.append(("host.docker.internal", 3306))
|
||||
targets.append(("127.0.0.1", 3306))
|
||||
seen: set[tuple[str, int]] = set()
|
||||
for host, port in targets:
|
||||
if (host, port) in seen:
|
||||
continue
|
||||
seen.add((host, port))
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=2.0):
|
||||
return True, f"✓ 已安装({host}:{port} 可访问)"
|
||||
except OSError:
|
||||
continue
|
||||
hint = (
|
||||
"host.docker.internal:3306"
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") == "1"
|
||||
else "127.0.0.1:3306"
|
||||
)
|
||||
return False, f"✗ 未检测到 MySQL({hint})"
|
||||
|
||||
|
||||
def _mysql_tcp_reachable(host: str, port: int, timeout: float = 3.0) -> bool:
|
||||
import socket
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _mysql_connect_error_detail(host: str, port: int) -> str:
|
||||
if os.environ.get("NEXUS_DOCKER_WRITE_ENV") != "1":
|
||||
return ""
|
||||
if host not in ("host.docker.internal", "172.17.0.1"):
|
||||
return ""
|
||||
return (
|
||||
f"容器无法 TCP 连接宿主机 MySQL({host}:{port})。"
|
||||
"请先在 1Panel 应用商店安装 MySQL 并确保服务运行;"
|
||||
"创建数据库 nexus 与用户 nexus 后,在安装向导步骤 3 填写密码。"
|
||||
"服务器上可执行: docker exec nexus-prod-nexus-1 sh -c "
|
||||
f"'timeout 2 bash -c \"</dev/tcp/{host}/{port}\"' 验证端口。"
|
||||
)
|
||||
|
||||
|
||||
def _parse_dotenv(path: Path) -> dict[str, str]:
|
||||
data: dict[str, str] = {}
|
||||
if not path.is_file():
|
||||
@@ -521,12 +571,14 @@ async def env_check():
|
||||
"blocks_wizard": False,
|
||||
})
|
||||
|
||||
# MySQL — 步骤3配置,步骤4验证连接
|
||||
# MySQL — 步骤2 TCP 检测;步骤4再验证账号密码
|
||||
mysql_installed, mysql_msg = _probe_mysql_installed()
|
||||
checks.append({
|
||||
"name": "MySQL 数据库",
|
||||
"required": "步骤3配置",
|
||||
"current": "— 步骤4将检测连接",
|
||||
"pass": True,
|
||||
"name": "MySQL(宿主机)",
|
||||
"required": "建议已安装",
|
||||
"current": mysql_msg,
|
||||
"pass": mysql_installed,
|
||||
"blocks_wizard": False,
|
||||
})
|
||||
|
||||
# Write permissions
|
||||
@@ -583,6 +635,14 @@ async def init_db(req: InitDbRequest):
|
||||
db_url = _make_db_url(req)
|
||||
redis_url = _build_redis_url(req)
|
||||
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
|
||||
db_port = int(req.db_port)
|
||||
|
||||
if not _mysql_tcp_reachable(req.db_host, db_port):
|
||||
detail = _mysql_connect_error_detail(req.db_host, db_port)
|
||||
raise HTTPException(
|
||||
400,
|
||||
detail or f"无法连接 MySQL 服务器 {req.db_host}:{db_port}(TCP 不通,请确认服务已启动)",
|
||||
)
|
||||
|
||||
try:
|
||||
patch_aiomysql_do_ping()
|
||||
@@ -673,6 +733,11 @@ async def init_db(req: InitDbRequest):
|
||||
|
||||
except Exception as e:
|
||||
await engine.dispose()
|
||||
err = str(e)
|
||||
if "2003" in err or "Can't connect to MySQL" in err:
|
||||
extra = _mysql_connect_error_detail(req.db_host, db_port)
|
||||
if extra:
|
||||
raise HTTPException(400, extra) from e
|
||||
raise HTTPException(400, f"数据库初始化失败: {e}") from e
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user