Created
October 27, 2024 08:14
-
-
Save abdalrohman/89c9efd378a761cf4cab62160bd45367 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import ctypes | |
| import json | |
| import logging | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import time | |
| import winreg | |
| from datetime import datetime | |
| from typing import Dict, List | |
| import psutil | |
| import win32gui | |
| import win32process | |
| class WindowsProgramManager: | |
| def __init__(self): | |
| self.logger = self._setup_logging() | |
| self._verify_admin() | |
| self.uninstall_registry_paths = [ | |
| (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), | |
| (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), | |
| (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), | |
| ] | |
| self.program_cache_file = os.path.join(os.getenv("APPDATA"), "program_usage_cache.json") | |
| self.usage_data = self._load_usage_data() | |
| def _setup_logging(self) -> logging.Logger: | |
| """Setup logging configuration""" | |
| log_dir = os.path.join(os.environ["USERPROFILE"], "Documents", "ProgramManagerLogs") | |
| os.makedirs(log_dir, exist_ok=True) | |
| log_file = os.path.join(log_dir, f'program_manager_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log') | |
| logger = logging.getLogger("ProgramManager") | |
| logger.setLevel(logging.INFO) | |
| file_handler = logging.FileHandler(log_file) | |
| console_handler = logging.StreamHandler() | |
| formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") | |
| file_handler.setFormatter(formatter) | |
| console_handler.setFormatter(formatter) | |
| logger.addHandler(file_handler) | |
| logger.addHandler(console_handler) | |
| return logger | |
| def _verify_admin(self): | |
| """Verify and request admin privileges if needed""" | |
| if not ctypes.windll.shell32.IsUserAnAdmin(): | |
| self.logger.info("Requesting administrator privileges...") | |
| ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(sys.argv), None, 1) | |
| sys.exit(0) | |
| def _load_usage_data(self) -> Dict: | |
| """Load program usage data from cache""" | |
| if os.path.exists(self.program_cache_file): | |
| try: | |
| with open(self.program_cache_file, "r") as f: | |
| return json.load(f) | |
| except json.JSONDecodeError: | |
| return {} | |
| return {} | |
| def _save_usage_data(self): | |
| """Save program usage data to cache""" | |
| with open(self.program_cache_file, "w") as f: | |
| json.dump(self.usage_data, f) | |
| def _get_program_details(self, key_handle, subkey) -> Dict: | |
| """Get program details from registry""" | |
| try: | |
| with winreg.OpenKey(key_handle, subkey) as key: | |
| details = {} | |
| try: | |
| details["DisplayName"] = winreg.QueryValueEx(key, "DisplayName")[0] | |
| except FileNotFoundError: | |
| return None | |
| try: | |
| details["UninstallString"] = winreg.QueryValueEx(key, "UninstallString")[0] | |
| except FileNotFoundError: | |
| details["UninstallString"] = None | |
| try: | |
| details["InstallLocation"] = winreg.QueryValueEx(key, "InstallLocation")[0] | |
| except FileNotFoundError: | |
| details["InstallLocation"] = None | |
| try: | |
| details["Publisher"] = winreg.QueryValueEx(key, "Publisher")[0] | |
| except FileNotFoundError: | |
| details["Publisher"] = "Unknown" | |
| try: | |
| details["InstallDate"] = winreg.QueryValueEx(key, "InstallDate")[0] | |
| except FileNotFoundError: | |
| details["InstallDate"] = None | |
| details["RegistryPath"] = subkey | |
| return details | |
| except WindowsError: | |
| return None | |
| def get_installed_programs(self) -> List[Dict]: | |
| """Get list of all installed programs""" | |
| programs = [] | |
| seen_names = set() | |
| for hkey, reg_path in self.uninstall_registry_paths: | |
| try: | |
| with winreg.OpenKey(hkey, reg_path) as key: | |
| for i in range(winreg.QueryInfoKey(key)[0]): | |
| try: | |
| subkey_name = winreg.EnumKey(key, i) | |
| details = self._get_program_details(hkey, f"{reg_path}\\{subkey_name}") | |
| if details and details["DisplayName"] not in seen_names: | |
| seen_names.add(details["DisplayName"]) | |
| programs.append(details) | |
| except WindowsError: | |
| continue | |
| except WindowsError: | |
| continue | |
| return programs | |
| def update_program_usage(self): | |
| """Update program usage statistics""" | |
| def get_process_path(pid): | |
| try: | |
| return psutil.Process(pid).exe() | |
| except (psutil.NoSuchProcess, psutil.AccessDenied): | |
| return None | |
| def window_callback(hwnd, _): | |
| if win32gui.IsWindowVisible(hwnd): | |
| try: | |
| _, pid = win32process.GetWindowThreadProcessId(hwnd) | |
| path = get_process_path(pid) | |
| if path: | |
| running_programs.add(path.lower()) | |
| except Exception: | |
| pass | |
| running_programs = set() | |
| win32gui.EnumWindows(window_callback, None) | |
| current_time = datetime.now().isoformat() | |
| for program in running_programs: | |
| if program not in self.usage_data: | |
| self.usage_data[program] = {"last_used": current_time, "usage_count": 1} | |
| else: | |
| self.usage_data[program]["last_used"] = current_time | |
| self.usage_data[program]["usage_count"] += 1 | |
| self._save_usage_data() | |
| def find_unused_programs(self, days_threshold: int = 90) -> List[Dict]: | |
| """Find programs that haven't been used recently""" | |
| current_time = datetime.now() | |
| unused_programs = [] | |
| installed_programs = self.get_installed_programs() | |
| for program in installed_programs: | |
| if program["InstallLocation"]: | |
| exe_files = self._find_exe_files(program["InstallLocation"]) | |
| is_unused = True | |
| for exe in exe_files: | |
| exe_path = exe.lower() | |
| if exe_path in self.usage_data: | |
| last_used = datetime.fromisoformat(self.usage_data[exe_path]["last_used"]) | |
| if (current_time - last_used).days < days_threshold: | |
| is_unused = False | |
| break | |
| if is_unused: | |
| unused_programs.append(program) | |
| return unused_programs | |
| def _find_exe_files(self, directory: str) -> List[str]: | |
| """Find all executable files in a directory""" | |
| exe_files = [] | |
| if not directory: | |
| return exe_files | |
| try: | |
| for root, _, files in os.walk(directory): | |
| for file in files: | |
| if file.lower().endswith(".exe"): | |
| exe_files.append(os.path.join(root, file)) | |
| except Exception as e: | |
| self.logger.error(f"Error searching for exe files in {directory}: {str(e)}") | |
| return exe_files | |
| def _clean_leftover_files(self, program: Dict) -> None: | |
| """Clean leftover files after uninstallation""" | |
| directories_to_check = [ | |
| os.path.join(os.environ["ProgramData"], program["DisplayName"]), | |
| os.path.join(os.environ["LOCALAPPDATA"], program["DisplayName"]), | |
| os.path.join(os.environ["APPDATA"], program["DisplayName"]), | |
| program["InstallLocation"] if program["InstallLocation"] else None, | |
| ] | |
| for directory in directories_to_check: | |
| if directory and os.path.exists(directory): | |
| try: | |
| self.logger.info(f"Removing directory: {directory}") | |
| shutil.rmtree(directory) | |
| except Exception as e: | |
| self.logger.error(f"Error removing directory {directory}: {str(e)}") | |
| # Clean registry | |
| try: | |
| with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, program["RegistryPath"], 0, winreg.KEY_ALL_ACCESS) as key: | |
| winreg.DeleteKey(key, "") | |
| except WindowsError as e: | |
| self.logger.error(f"Error cleaning registry for {program['DisplayName']}: {str(e)}") | |
| def uninstall_program(self, program: Dict) -> bool: | |
| """Uninstall a program and clean up related files""" | |
| if not program["UninstallString"]: | |
| self.logger.error(f"No uninstall string found for {program['DisplayName']}") | |
| return False | |
| try: | |
| # Handle different types of uninstall strings | |
| uninstall_cmd = program["UninstallString"] | |
| if uninstall_cmd.startswith('"'): | |
| uninstall_cmd = f"{uninstall_cmd} /quiet /norestart" | |
| elif "msiexec.exe" in uninstall_cmd.lower(): | |
| uninstall_cmd = f"{uninstall_cmd} /quiet /norestart" | |
| else: | |
| uninstall_cmd = f'"{uninstall_cmd}" /S' | |
| self.logger.info(f"Uninstalling {program['DisplayName']}...") | |
| process = subprocess.Popen(uninstall_cmd, shell=True) | |
| process.wait(timeout=300) # 5-minute timeout | |
| if process.returncode == 0: | |
| self.logger.info(f"Successfully uninstalled {program['DisplayName']}") | |
| time.sleep(2) # Wait for uninstaller to finish | |
| self._clean_leftover_files(program) | |
| return True | |
| else: | |
| self.logger.error(f"Failed to uninstall {program['DisplayName']}") | |
| return False | |
| except subprocess.TimeoutExpired: | |
| self.logger.error(f"Uninstall timeout for {program['DisplayName']}") | |
| return False | |
| except Exception as e: | |
| self.logger.error(f"Error uninstalling {program['DisplayName']}: {str(e)}") | |
| return False | |
| def main(): | |
| manager = WindowsProgramManager() | |
| # Update program usage data | |
| print("Updating program usage statistics...") | |
| manager.update_program_usage() | |
| # Get unused programs | |
| print("\nSearching for unused programs...") | |
| unused_programs = manager.find_unused_programs() | |
| if not unused_programs: | |
| print("No unused programs found.") | |
| return | |
| # Display unused programs | |
| print("\nUnused Programs:") | |
| for i, program in enumerate(unused_programs, 1): | |
| print(f"{i}. {program['DisplayName']} (Publisher: {program['Publisher']})") | |
| if program["InstallDate"]: | |
| print(f" Installed: {program['InstallDate']}") | |
| print(f" Location: {program['InstallLocation'] or 'Unknown'}") | |
| print() | |
| # Get user selection | |
| while True: | |
| try: | |
| selection = input("Enter the numbers of programs to uninstall (comma-separated) or 'q' to quit: ") | |
| if selection.lower() == "q": | |
| return | |
| indices = [int(x.strip()) - 1 for x in selection.split(",")] | |
| selected_programs = [unused_programs[i] for i in indices if 0 <= i < len(unused_programs)] | |
| break | |
| except (ValueError, IndexError): | |
| print("Invalid selection. Please try again.") | |
| # Confirm uninstallation | |
| print("\nPrograms selected for uninstallation:") | |
| for program in selected_programs: | |
| print(f"- {program['DisplayName']}") | |
| confirm = input("\nAre you sure you want to uninstall these programs? (yes/no): ") | |
| if confirm.lower() != "yes": | |
| print("Uninstallation cancelled.") | |
| return | |
| # Perform uninstallation | |
| print("\nUninstalling selected programs...") | |
| for program in selected_programs: | |
| if manager.uninstall_program(program): | |
| print(f"Successfully uninstalled {program['DisplayName']}") | |
| else: | |
| print(f"Failed to uninstall {program['DisplayName']}") | |
| print("\nUninstallation process completed!") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment