diff --git a/backend/routers/updates.py b/backend/routers/updates.py index c0f6a81..f60cf4f 100644 --- a/backend/routers/updates.py +++ b/backend/routers/updates.py @@ -147,26 +147,45 @@ def _parse_apt_packages(install_sh_content: str) -> set[str]: async def _get_installed_packages(packages: set[str]) -> set[str]: - """Check which packages from the set are already installed via dpkg.""" + """Check which packages from the set are already installed via dpkg-query.""" if not packages: return set() installed = set() - for pkg in packages: + # Use dpkg-query for batch check — more reliable than dpkg -s + # Note: dpkg-query returns non-zero if ANY package is unknown, but still + # outputs status for known packages on stdout. + try: proc = await asyncio.create_subprocess_exec( - "dpkg", "-s", pkg, + "dpkg-query", "-W", "-f", "${Package} ${Status}\n", *sorted(packages), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=_safe_env(), ) - try: - stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5) - except asyncio.TimeoutError: - continue - if proc.returncode == 0: - # Verify it's actually installed (Status: install ok installed) - output = stdout.decode() - if "Status:" in output and "installed" in output: - installed.add(pkg) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10) + # returncode may be non-zero if some packages are unknown — that's fine + for line in stdout.decode().strip().splitlines(): + # Format: "packagename install ok installed" + if "install ok installed" in line: + pkg_name = line.split()[0] + installed.add(pkg_name) + except (asyncio.TimeoutError, Exception) as e: + log.debug("dpkg-query failed, falling back to individual checks: %s", e) + # Fallback: check each package individually + for pkg in packages: + proc = await asyncio.create_subprocess_exec( + "dpkg", "-s", pkg, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=_safe_env(), + ) + try: + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5) + except asyncio.TimeoutError: + continue + if proc.returncode == 0: + output = stdout.decode() + if "install ok installed" in output: + installed.add(pkg) return installed @@ -262,6 +281,20 @@ async def install_deps(req: InstallDepsRequest, _user: str = Depends(get_current raise HTTPException(400, f"Invalid package name: {pkg}") apt_bin = shutil.which("apt-get") or "/usr/bin/apt-get" + + # Update package lists first + log.info("Running apt-get update before install") + update_proc = await asyncio.create_subprocess_exec( + apt_bin, "update", "-qq", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=_safe_env(), + ) + try: + await asyncio.wait_for(update_proc.communicate(), timeout=120) + except asyncio.TimeoutError: + pass # Non-fatal, proceed with install anyway + cmd = [apt_bin, "install", "-y"] + req.packages log.info("Installing system packages: %s", " ".join(req.packages)) diff --git a/install.sh b/install.sh index f9ba294..a5e9c42 100755 --- a/install.sh +++ b/install.sh @@ -52,6 +52,7 @@ install_deps() { xdotool \ imagemagick \ x11-utils \ + libxcb-cursor0 \ > /dev/null 2>&1 ok "System dependencies installed." }