diff --git a/backend/routers/network.py b/backend/routers/network.py index 24832a8..bcc6492 100644 --- a/backend/routers/network.py +++ b/backend/routers/network.py @@ -46,6 +46,17 @@ def _require_nmcli(): raise HTTPException(503, "NetworkManager (nmcli) not available on this system") +def _safe_env(): + """Build environment with full PATH for nmcli commands.""" + env = {**os.environ, "LC_ALL": "C"} + path = env.get("PATH", "") + for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"): + if p not in path: + path = p + ":" + path + env["PATH"] = path + return env + + async def _nmcli(*args: str, timeout: float = 30) -> str: """Run nmcli with C locale, return stdout. Raise HTTPException on failure.""" _require_nmcli() @@ -54,7 +65,7 @@ async def _nmcli(*args: str, timeout: float = 30) -> str: *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env={**os.environ, "LC_ALL": "C"}, + env=_safe_env(), ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) diff --git a/backend/routers/packages.py b/backend/routers/packages.py index 8a6fc72..28654bd 100644 --- a/backend/routers/packages.py +++ b/backend/routers/packages.py @@ -16,7 +16,19 @@ router = APIRouter(prefix="/api/packages", tags=["packages"]) # Guard # --------------------------------------------------------------------------- -_HAS_APT = bool(shutil.which("apt-cache")) +def _find_apt(): + """Find apt-cache binary, checking common paths if PATH is restricted.""" + found = shutil.which("apt-cache") + if found: + return True + # systemd services may have a minimal PATH — check directly + for p in ("/usr/bin/apt-cache", "/usr/sbin/apt-cache"): + if os.path.isfile(p) and os.access(p, os.X_OK): + return True + return False + + +_HAS_APT = _find_apt() def _require_apt(): @@ -38,6 +50,18 @@ def _validate_name(name: str) -> str: return name +def _safe_env(): + """Build environment with full PATH for apt/dpkg commands.""" + env = {**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"} + # Ensure standard paths are included (systemd may use minimal PATH) + path = env.get("PATH", "") + for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"): + if p not in path: + path = p + ":" + path + env["PATH"] = path + return env + + async def _apt(*args: str, timeout: float = 30) -> str: """Run an apt/dpkg command and return stdout.""" _require_apt() @@ -46,7 +70,7 @@ async def _apt(*args: str, timeout: float = 30) -> str: *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env={**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"}, + env=_safe_env(), ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) @@ -68,7 +92,7 @@ async def _apt_nofail(*args: str, timeout: float = 30) -> tuple[int, str, str]: *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env={**os.environ, "LC_ALL": "C", "DEBIAN_FRONTEND": "noninteractive"}, + env=_safe_env(), ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) diff --git a/backend/routers/updates.py b/backend/routers/updates.py index f1e6946..8fb9541 100644 --- a/backend/routers/updates.py +++ b/backend/routers/updates.py @@ -30,6 +30,17 @@ def _require_git(): # Helpers # --------------------------------------------------------------------------- +def _safe_env(): + """Build environment with full PATH for git commands.""" + env = {**os.environ, "LC_ALL": "C"} + path = env.get("PATH", "") + for p in ("/usr/bin", "/usr/sbin", "/bin", "/sbin"): + if p not in path: + path = p + ":" + path + env["PATH"] = path + return env + + async def _git(*args: str, timeout: float = 30) -> str: """Run a git command in the Atlus install directory.""" _require_git() @@ -38,7 +49,7 @@ async def _git(*args: str, timeout: float = 30) -> str: *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env={**os.environ, "LC_ALL": "C"}, + env=_safe_env(), ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) @@ -60,7 +71,7 @@ async def _git_nofail(*args: str, timeout: float = 30) -> tuple[int, str]: *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env={**os.environ, "LC_ALL": "C"}, + env=_safe_env(), ) try: stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)