[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
+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:
return WineStatusResponse(
wine_binary=binary,
resolved_binary=resolved,
available=False,
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
probe = await probe_wine_binary_async(resolved)
return WineStatusResponse(
wine_binary=binary,
resolved_binary=resolved,
available=process.returncode == 0,
version=version,
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=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"