Skip to content

Instantly share code, notes, and snippets.

@abdalrohman
Created October 27, 2024 08:14
Show Gist options
  • Select an option

  • Save abdalrohman/89c9efd378a761cf4cab62160bd45367 to your computer and use it in GitHub Desktop.

Select an option

Save abdalrohman/89c9efd378a761cf4cab62160bd45367 to your computer and use it in GitHub Desktop.
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