[fix]:[FL-48][it looks like wine32 is missing]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+4
-1
@@ -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 ./
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user