"""Инвентаризация установленных программ: размер на диске, давность использования.""" from __future__ import annotations import os import subprocess import sys from typing import Callable from .model import Program from .util import Cancel, dir_stats, known_paths _UNINSTALL_KEYS = ( ("HKLM", r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM"), ("HKLM", r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM-32"), ("HKCU", r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKCU"), ) def _read_registry() -> list[Program]: if sys.platform != "win32": return [] import winreg roots = {"HKLM": winreg.HKEY_LOCAL_MACHINE, "HKCU": winreg.HKEY_CURRENT_USER} out: list[Program] = [] seen: set[tuple[str, str]] = set() for root_name, subkey, label in _UNINSTALL_KEYS: try: key = winreg.OpenKey(roots[root_name], subkey, 0, winreg.KEY_READ) except OSError: continue with key: try: count = winreg.QueryInfoKey(key)[0] except OSError: continue for i in range(count): try: name = winreg.EnumKey(key, i) sub = winreg.OpenKey(key, name, 0, winreg.KEY_READ) except OSError: continue with sub: def val(field: str, default=""): try: return winreg.QueryValueEx(sub, field)[0] except OSError: return default display = str(val("DisplayName") or "").strip() if not display: continue if val("SystemComponent", 0) == 1: continue if val("ParentKeyName") or val("ParentDisplayName"): # обновления и компоненты внутри бандлов if not val("UninstallString"): continue version = str(val("DisplayVersion") or "") dedup = (display.lower(), version) if dedup in seen: continue seen.add(dedup) try: est = int(val("EstimatedSize", 0) or 0) * 1024 except (TypeError, ValueError): est = 0 out.append(Program( name=display, publisher=str(val("Publisher") or ""), version=version, install_date=str(val("InstallDate") or ""), install_location=str(val("InstallLocation") or "").strip('"'), uninstall_cmd=str(val("QuietUninstallString") or val("UninstallString") or ""), registry_size=est, source=label, )) return out def _guess_location(prog: Program) -> str: """Если InstallLocation пуст — пробуем вытащить путь из строки удаления.""" if prog.install_location and os.path.isdir(prog.install_location): return prog.install_location cmd = prog.uninstall_cmd if not cmd: return "" candidate = "" if cmd.startswith('"'): candidate = cmd[1:].split('"', 1)[0] else: parts = cmd.split(" ") for i in range(len(parts), 0, -1): guess = " ".join(parts[:i]) if os.path.exists(guess): candidate = guess break if not candidate: return "" folder = os.path.dirname(candidate) k = known_paths() # не отдаём общие корни вроде C:\Windows\System32 bad = {os.path.normcase(k["WINDIR"]), os.path.normcase(os.path.join(k["WINDIR"], "System32")), os.path.normcase(k["PROGRAMFILES"]), os.path.normcase(k["PROGRAMFILES86"]), os.path.normcase(k["TEMP"])} if not folder or os.path.normcase(folder) in bad or len(folder) <= 3: return "" return folder if os.path.isdir(folder) else "" def scan_programs( measure_disk: bool = True, cancel: Cancel | None = None, progress: Callable[[str], None] | None = None, ) -> list[Program]: """Список установленных программ, отсортированный по занимаемому месту.""" progs = _read_registry() if measure_disk: for i, p in enumerate(progs): if cancel is not None and cancel.cancelled: break loc = _guess_location(p) if not loc: continue p.install_location = loc size, _files, _dirs, mtime = dir_stats(loc, cancel, max_entries=60_000) p.disk_size = size p.last_used = mtime if progress is not None and i % 10 == 0: progress(p.name) progs.sort(key=lambda p: -p.best_size) return progs def store_apps() -> list[Program]: """Приложения Microsoft Store (через PowerShell, может быть медленно).""" if sys.platform != "win32": return [] ps = ( "Get-AppxPackage | Select-Object Name,Publisher,Version,InstallLocation | " "ConvertTo-Csv -NoTypeInformation" ) try: res = subprocess.run( ["powershell", "-NoProfile", "-NonInteractive", "-Command", ps], capture_output=True, text=True, timeout=90, errors="ignore", creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) except Exception: return [] import csv import io out: list[Program] = [] try: for row in csv.DictReader(io.StringIO(res.stdout)): loc = (row.get("InstallLocation") or "").strip() if not loc or not os.path.isdir(loc): continue size, _f, _d, mtime = dir_stats(loc, max_entries=30_000) out.append(Program( name=row.get("Name") or "?", publisher=(row.get("Publisher") or "").split(",")[0].replace("CN=", ""), version=row.get("Version") or "", install_location=loc, disk_size=size, last_used=mtime, source="Store", )) except Exception: return out out.sort(key=lambda p: -p.best_size) return out def uninstall(prog: Program) -> tuple[bool, str]: """Запустить штатный деинсталлятор программы (интерактивно).""" if not prog.uninstall_cmd: return False, "у программы нет команды удаления в реестре" try: subprocess.Popen(prog.uninstall_cmd, shell=True) return True, "" except Exception as e: # noqa: BLE001 return False, str(e)