[fix]:[FL-48][it looks like wine32 is missing]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-08 22:16:02 +08:00
parent de63459173
commit 23980a3cf3
7 changed files with 146 additions and 26 deletions
+2 -1
View File
@@ -242,7 +242,8 @@
- `WINE_DEFAULT_TIMEOUT_SECONDS` / `WINE_MAX_TIMEOUT_SECONDS`:默认与最大执行超时。
- 后端执行必须使用 `asyncio.create_subprocess_exec` 参数数组,不走 shell;EXE 路径与工作目录必须限制在 `WINE_ALLOWED_ROOT` 下。
- 实时日志通过 `StreamingResponse` + SSE 事件输出;前端使用 `fetchWithAuth` 读取 `ReadableStream`,避免原生 `EventSource` 无法携带现有 Bearer Token 的鉴权问题。
- `api/Dockerfile` 构建出的共享后端镜像已内置 `wine``api/celery-worker/celery-beat/flower` 当前都复用该镜像,若需切换二进制仍可通过 `WINE_BINARY_PATH` 覆盖。
- `api/Dockerfile` 构建出的共享后端镜像已内置 `wine + wine32:i386``api/celery-worker/celery-beat/flower` 当前都复用该镜像,若需切换二进制仍可通过 `WINE_BINARY_PATH` 覆盖。
- Wine / ATP 的状态探测口径中,若 `wine --version` 输出包含 `wine32 is missing`,必须标记为不可用,避免前端把半可用环境误显示为“可用”。
## 前端菜单交互口径(2026-04-19
+4 -1
View File
@@ -13,11 +13,14 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV PIP_DEFAULT_TIMEOUT=${PIP_DEFAULT_TIMEOUT}
ENV PIP_RETRIES=${PIP_RETRIES}
RUN apt-get update \
RUN dpkg --add-architecture i386 \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libexpat1 \
wine \
wine32:i386 \
&& command -v wine >/dev/null \
&& dpkg-query -W -f='${Status}' wine32:i386 | grep -q "install ok installed" \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
+3 -1
View File
@@ -35,6 +35,7 @@ from ..schemas.atp_model import (
AtpSimulationRunSummary,
)
from .push_service import publish_topic
from .wine_probe import probe_wine_binary
settings = get_settings()
@@ -164,7 +165,8 @@ def _resolve_wine_engine_executable() -> tuple[str | None, str | None, str | Non
if not configured.exists() or not configured.is_file():
return wine_binary, None, f"ATP engine executable not found: {configured}"
return wine_binary, str(configured), None
probe = probe_wine_binary(wine_binary)
return wine_binary, str(configured), None if probe.available else (probe.error or "Wine binary unavailable")
def _resolve_native_engine_executable() -> tuple[str | None, str | None]:
+66
View File
@@ -0,0 +1,66 @@
from __future__ import annotations
import asyncio
import subprocess
from dataclasses import dataclass
_WINE32_MISSING_HINTS = (
"it looks like wine32 is missing",
"wine32 is missing",
)
@dataclass(slots=True)
class WineProbeResult:
available: bool
version: str | None
error: str | None
def interpret_wine_probe_output(returncode: int, output: bytes | str | None) -> WineProbeResult:
if isinstance(output, bytes):
text = output.decode("utf-8", errors="replace")
else:
text = output or ""
normalized = text.strip() or None
if normalized:
lowered = normalized.lower()
if any(hint in lowered for hint in _WINE32_MISSING_HINTS):
return WineProbeResult(available=False, version=normalized, error=normalized)
if returncode != 0:
return WineProbeResult(available=False, version=normalized, error=normalized)
return WineProbeResult(available=True, version=normalized, error=None)
def probe_wine_binary(resolved_binary: str, *, timeout_seconds: int = 10) -> WineProbeResult:
try:
completed = subprocess.run(
[resolved_binary, "--version"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
timeout=timeout_seconds,
)
except (OSError, subprocess.TimeoutExpired) as exc:
return WineProbeResult(available=False, version=None, error=str(exc))
return interpret_wine_probe_output(completed.returncode, completed.stdout)
async def probe_wine_binary_async(resolved_binary: str, *, timeout_seconds: int = 10) -> WineProbeResult:
try:
process = await asyncio.create_subprocess_exec(
resolved_binary,
"--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
output, _ = await asyncio.wait_for(process.communicate(), timeout=timeout_seconds)
except (OSError, asyncio.TimeoutError) as exc:
return WineProbeResult(available=False, version=None, error=str(exc))
return interpret_wine_probe_output(process.returncode, output)
+10 -23
View File
@@ -13,6 +13,7 @@ from fastapi import HTTPException, status
from ..core.config import get_settings
from ..schemas.wine import WineRunRequest, WineStatusResponse
from .wine_probe import probe_wine_binary_async
settings = get_settings()
@@ -106,35 +107,16 @@ async def get_wine_status() -> WineStatusResponse:
error="Wine binary not found",
)
try:
process = await asyncio.create_subprocess_exec(
resolved,
"--version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
output, _ = await asyncio.wait_for(process.communicate(), timeout=10)
except (OSError, asyncio.TimeoutError) as exc:
probe = await probe_wine_binary_async(resolved)
return WineStatusResponse(
wine_binary=binary,
resolved_binary=resolved,
available=False,
available=probe.available,
version=probe.version,
allowed_root=allowed_root,
default_timeout_seconds=settings.wine_default_timeout_seconds,
max_timeout_seconds=settings.wine_max_timeout_seconds,
error=str(exc),
)
version = output.decode("utf-8", errors="replace").strip() or None
return WineStatusResponse(
wine_binary=binary,
resolved_binary=resolved,
available=process.returncode == 0,
version=version,
allowed_root=allowed_root,
default_timeout_seconds=settings.wine_default_timeout_seconds,
max_timeout_seconds=settings.wine_max_timeout_seconds,
error=None if process.returncode == 0 else version,
error=probe.error,
)
@@ -156,6 +138,11 @@ async def stream_wine_run(payload: WineRunRequest) -> AsyncGenerator[str, None]:
yield _sse_event("error", {"run_id": run_id, "message": "Wine binary not found"})
return
probe = await probe_wine_binary_async(resolved_binary)
if not probe.available:
yield _sse_event("error", {"run_id": run_id, "message": probe.error or "Wine binary unavailable"})
return
try:
executable = _resolve_executable(payload.exe_path)
working_dir = _resolve_working_dir(payload.working_dir, executable)
+33
View File
@@ -0,0 +1,33 @@
from __future__ import annotations
from app.services.wine_probe import interpret_wine_probe_output
def test_interpret_wine_probe_output_marks_wine32_warning_unavailable() -> None:
output = (
"it looks like wine32 is missing, you should install it. multiarch needs to be enabled first.\n"
'as root, please execute "dpkg --add-architecture i386 && apt-get update && apt-get install wine32:i386"\n'
"wine-10.0 (Debian 10.0~repack-6)"
)
result = interpret_wine_probe_output(0, output)
assert result.available is False
assert result.version == output
assert result.error == output
def test_interpret_wine_probe_output_accepts_normal_version() -> None:
result = interpret_wine_probe_output(0, "wine-10.0 (Debian 10.0~repack-6)")
assert result.available is True
assert result.version == "wine-10.0 (Debian 10.0~repack-6)"
assert result.error is None
def test_interpret_wine_probe_output_propagates_nonzero_exit() -> None:
result = interpret_wine_probe_output(1, "wine: failed to load")
assert result.available is False
assert result.version == "wine: failed to load"
assert result.error == "wine: failed to load"
+28
View File
@@ -64,3 +64,31 @@
- 风险与关注点:
- 由于部署链路复用同一个后端镜像,本次会同时影响 `api/celery-worker/celery-beat/flower` 的基础镜像体积,而不只是 `celery-worker`
## Work Log - 修正 Wine 缺少 wine32 时的误报(2026-06-08
- 背景:
- 当前环境里 `wine --version` 会输出 `it looks like wine32 is missing`,但后端 `/api/v1/wine/status` 之前只按返回码判定,可能把半可用环境误报成“可用”。
- `ATP` 的 Wine 引擎状态也复用同一 Wine 二进制,存在同类误报风险。
- 本次处理:
- `api/app/services/wine_probe.py`
- 新增共享 Wine 探测 helper,统一将 `wine32 is missing` 识别为不可用状态。
- `api/app/services/wine_service.py`
- Wine 状态接口与测试执行前校验改为复用共享探测 helper。
- `api/app/services/atp_model_service.py`
- ATP Wine 引擎状态与执行前校验同步复用共享探测 helper。
- `api/Dockerfile`
- 构建阶段显式开启 `i386` 并安装 `wine32:i386`,避免镜像只装到半套 Wine 运行时。
- `api/tests/test_wine_probe.py`
- 新增针对 `wine32 is missing` 误报场景的最小单测。
- 验证:
- 变更前基线:`python3 -m pytest api/tests/test_fl_analysis_external.py``python3 -m pytest api/tests/test_fl_analysis_service.py` 因本地运行环境缺少 `pytest/sqlalchemy` 无法执行。
- 变更后执行:
- `python3 -m py_compile api/app/services/wine_probe.py api/app/services/wine_service.py api/app/services/atp_model_service.py api/tests/test_wine_probe.py` 通过。
- `./.venv/bin/python -m pytest api/tests/test_wine_probe.py` 通过(3 passed)。
- `git diff --check` 通过。
- 风险与关注点:
- 当前本地环境仍不具备完整后端依赖,无法直接回归所有 FastAPI/SQLAlchemy 相关测试;本次验证聚焦在 Wine 探测逻辑和语法层面。