import os import subprocess import time import logging import traceback import ctypes try: import _winreg except ImportError: import winreg as _winreg # --------------------------------------------------------------------------- # Logging setup # --------------------------------------------------------------------------- LOG_DIR = r"C:\ProgramData\SPM_Logs" LOG_FILE = os.path.join(LOG_DIR, "freemake_uninstall.log") try: if not os.path.exists(LOG_DIR): os.makedirs(LOG_DIR) except Exception: pass logging.basicConfig( filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) log = logging.getLogger("freemake_uninstall") console = logging.StreamHandler() console.setLevel(logging.INFO) console.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', '%H:%M:%S')) log.addHandler(console) # Extensions/leftovers considered harmless Inno Setup uninstaller stubs # rather than actual application content IGNORABLE_EXTENSIONS = ('.dat', '.log', '.tmp') IGNORABLE_SIZE_LIMIT = 5 * 1024 * 1024 # 5MB — generous ceiling for a stub/log file def find_freemake_uninst(): reg_paths = [ (r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", _winreg.KEY_READ | _winreg.KEY_WOW64_32KEY), (r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", _winreg.KEY_READ | _winreg.KEY_WOW64_64KEY), ] for reg_path, flags in reg_paths: try: key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, reg_path, 0, flags) i = 0 while True: try: subkey_name = _winreg.EnumKey(key, i) subkey = _winreg.OpenKey(key, subkey_name) try: dn = _winreg.QueryValueEx(subkey, "DisplayName")[0] if "Freemake Video Converter" in dn: us = _winreg.QueryValueEx(subkey, "UninstallString")[0] _winreg.CloseKey(subkey) _winreg.CloseKey(key) us = us.strip() if us.startswith('"'): exe = us[1:us.index('"', 1)] else: exe = us.split()[0] log.info("Found registry entry '%s' -> uninstaller: %s", subkey_name, exe) return exe, subkey_name except Exception as e: log.debug("Skipping subkey %s (no matching DisplayName): %s", subkey_name, e) _winreg.CloseKey(subkey) i += 1 except WindowsError: break _winreg.CloseKey(key) except Exception as e: log.debug("Could not open uninstall hive (%s, flags=%s): %s", reg_path, flags, e) log.warning("No registry uninstall entry found for Freemake Video Converter") return None, None def kill_freemake_processes(): log.info("Killing Freemake-related processes") for cmd in ( ['Taskkill', '/IM', 'FreemakeVC.exe', '/F'], ['Taskkill', '/IM', 'unins000.exe', '/F'], ['Taskkill', '/F', '/FI', 'WINDOWTITLE eq Setup/Uninstall*'], ): try: rc = subprocess.call(cmd, stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) log.debug("Command %s returned %s", cmd, rc) except Exception as e: log.debug("Command %s raised: %s", cmd, e) time.sleep(2) def robocopy_wipe(folder): """ Deletes a stubborn folder by mirroring an empty temp dir onto it with robocopy, then removing the (now-empty) root. Handles long paths and many locked-handle cases that 'rd /s /q' silently fails on. """ if not os.path.exists(folder): return True log.info("Attempting robocopy-mirror wipe on: %s", folder) empty_dir = os.path.join(os.environ.get('TEMP', r'C:\Windows\Temp'), 'empty_wipe_dir') try: if not os.path.exists(empty_dir): os.makedirs(empty_dir) rc = subprocess.call( ['robocopy', empty_dir, folder, '/MIR', '/R:1', '/W:1', '/NFL', '/NDL', '/NJH', '/NJS'], stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w') ) log.debug("robocopy exit code: %s", rc) subprocess.call(['cmd', '/c', 'rd', folder]) except Exception as e: log.error("robocopy_wipe failed on %s: %s", folder, e) log.debug(traceback.format_exc()) still_there = os.path.exists(folder) if still_there: log.warning("Folder still present after robocopy wipe: %s", folder) else: log.info("Folder successfully removed via robocopy wipe: %s", folder) return not still_there def force_delete_folder(folder): if not os.path.exists(folder): log.debug("Folder already absent, nothing to delete: %s", folder) return True log.info("Force-deleting folder: %s", folder) subprocess.call(['cmd', '/c', 'rd', '/s', '/q', folder]) time.sleep(2) if not os.path.exists(folder): log.info("Folder removed with plain 'rd /s /q': %s", folder) return True log.warning("'rd /s /q' did not remove folder, retrying with takeown/icacls: %s", folder) rc1 = subprocess.call(['takeown', '/f', folder, '/r', '/d', 'y'], stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) rc2 = subprocess.call(['icacls', folder, '/grant', 'administrators:F', '/t'], stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w')) log.debug("takeown rc=%s, icacls rc=%s", rc1, rc2) subprocess.call(['cmd', '/c', 'rd', '/s', '/q', folder]) time.sleep(2) if not os.path.exists(folder): log.info("Folder removed after takeown/icacls: %s", folder) return True log.warning("Folder still present after takeown/icacls retry, falling back to robocopy: %s", folder) return robocopy_wipe(folder) def delete_registry_key(subkey_name): log.info("Deleting registry uninstall key: %s", subkey_name) for flags in [_winreg.KEY_WOW64_32KEY, _winreg.KEY_WOW64_64KEY]: try: key = _winreg.OpenKey( _winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", 0, _winreg.KEY_ALL_ACCESS | flags ) _winreg.DeleteKey(key, subkey_name) _winreg.CloseKey(key) log.info("Registry key deleted (flags=%s)", flags) except Exception as e: log.debug("Registry key delete failed (flags=%s): %s", flags, e) def schedule_delete_on_reboot(path): """ Uses MoveFileEx with MOVEFILE_DELAY_UNTIL_REBOOT to register a file/folder for deletion via PendingFileRenameOperations on next reboot. Used for harmless Inno Setup leftover stubs (e.g. unins000.dat) that resist deletion because the uninstaller process itself still had a handle on them at the moment it was terminated. """ MOVEFILE_DELAY_UNTIL_REBOOT = 0x4 try: result = ctypes.windll.kernel32.MoveFileExW(unicode(path), None, MOVEFILE_DELAY_UNTIL_REBOOT) if result: log.info("Scheduled for deletion on next reboot (PFRO): %s", path) else: log.warning("Failed to schedule PFRO delete for: %s (error=%s)", path, ctypes.GetLastError()) return bool(result) except Exception as e: log.debug("schedule_delete_on_reboot failed for %s: %s", path, e) return False def get_remaining_meaningful_content(folder): """ Walks the folder and returns a list of items that are NOT harmless Inno Setup stub leftovers (unins000.dat/.log/.tmp under the size ceiling). Any subfolder, or any file outside that allowlist, counts as meaningful content that still needs attention. Returns [] if the folder is gone or only contains ignorable stubs. """ if not os.path.exists(folder): return [] meaningful = [] for root, dirs, files in os.walk(folder): for f in files: full = os.path.join(root, f) ext = os.path.splitext(f)[1].lower() try: size = os.path.getsize(full) except OSError: size = 0 if ext in IGNORABLE_EXTENSIONS and size <= IGNORABLE_SIZE_LIMIT: continue meaningful.append(full) return meaningful def cleanup_stub_leftovers(folder): """ For a folder that only contains harmless Inno Setup stub files, schedule those stubs (and the folder itself) for deletion on next reboot, so the machine ends up clean eventually without blocking the current run's success status. """ if not os.path.exists(folder): return stub_files = [] for root, dirs, files in os.walk(folder): for f in files: stub_files.append(os.path.join(root, f)) for f in stub_files: schedule_delete_on_reboot(f) # Schedule subfolders bottom-up, then the root itself all_dirs = [] for root, dirs, files in os.walk(folder): all_dirs.append(root) for d in sorted(all_dirs, key=len, reverse=True): schedule_delete_on_reboot(d) def wait_until_gone(paths, timeout=20, interval=1): """ Polls repeatedly instead of checking once, AND treats a folder that only contains harmless Inno Setup stub leftovers (unins000.dat etc.) as effectively gone rather than a failure. """ elapsed = 0 while elapsed < timeout: remaining = [p for p in paths if get_remaining_meaningful_content(p)] if not remaining: log.info("All target folders confirmed clear (gone or stub-only) after %ss", elapsed) return True time.sleep(interval) elapsed += interval remaining = [p for p in paths if get_remaining_meaningful_content(p)] if remaining: log.warning("Meaningful content still present after %ss poll: %s", timeout, remaining) return not remaining # dir: string -> directory where installer exist # args: string -> which includes all parameters with space delimiter def spm_install(dir, args): log.info("=== spm_install started (dir=%s, args=%s) ===", dir, args) os.chdir(dir) argslist = args.split() try: subprocess.check_call(argslist) time.sleep(60) kill_freemake_processes() log.info("Install completed successfully") print 'retcode' + str(0) + 'retcode' except subprocess.CalledProcessError as e: log.error("Install failed with return code %s", e.returncode) print 'retcode' + str(e.returncode) + 'retcode' # dir: string -> directory where installer exist # args: string -> which includes all parameters with space delimiter def spm_uninstall(dir, args): log.info("=== spm_uninstall started ===") target_folders = [ r"C:\Program Files (x86)\Freemake", r"C:\Program Files\Freemake", ] # Step 1: Kill all Freemake processes kill_freemake_processes() uninst_exe, reg_subkey = find_freemake_uninst() # Fallback paths if uninst_exe is None or not os.path.exists(uninst_exe): log.info("Registry lookup did not yield a valid uninstaller, checking fallback paths") for p in [ r"C:\Program Files (x86)\Freemake\Freemake Video Converter\Uninstall\unins000.exe", r"C:\Program Files\Freemake\Freemake Video Converter\Uninstall\unins000.exe", ]: if os.path.exists(p): uninst_exe = p log.info("Found fallback uninstaller: %s", p) break # Step 2: Run uninstaller with timeout (30 sec max) if uninst_exe and os.path.exists(uninst_exe): log.info("Launching uninstaller: %s", uninst_exe) try: proc = subprocess.Popen([ uninst_exe, '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-' ]) timeout = 30 elapsed = 0 while elapsed < timeout: if proc.poll() is not None: log.info("Uninstaller process exited after %ss (exit code %s)", elapsed, proc.poll()) break time.sleep(1) elapsed += 1 if proc.poll() is None: log.warning("Uninstaller did not exit within %ss, terminating", timeout) proc.terminate() time.sleep(2) kill_freemake_processes() else: log.info("Waiting 8s for Inno Setup background temp-copy cleanup to finish") time.sleep(8) except Exception as e: log.error("Exception while running uninstaller: %s", e) log.debug(traceback.format_exc()) else: log.warning("No uninstaller executable found, proceeding straight to forced removal") kill_freemake_processes() # Step 3: Force delete folders (with retry-capable logic inside) for folder in target_folders: force_delete_folder(folder) # Step 4: Delete registry key if reg_subkey: delete_registry_key(reg_subkey) else: log.debug("No registry subkey to delete") # Step 5: Poll instead of single check — now ignores harmless stub leftovers freemake_gone = wait_until_gone(target_folders, timeout=20, interval=1) # One more force-delete pass if real content is still hanging on if not freemake_gone: log.warning("First verification failed, running a second force-delete pass") for folder in target_folders: force_delete_folder(folder) freemake_gone = wait_until_gone(target_folders, timeout=10, interval=1) # If only stub leftovers remain (e.g. locked unins000.dat), schedule # them for deletion on next reboot instead of failing the run if freemake_gone: for folder in target_folders: if os.path.exists(folder): log.info("Folder still physically present but only contains harmless stub " "leftovers, scheduling for reboot cleanup: %s", folder) cleanup_stub_leftovers(folder) if freemake_gone: log.info("=== spm_uninstall finished: SUCCESS ===") print 'retcode' + str(0) + 'retcode' else: remaining = {} for p in target_folders: content = get_remaining_meaningful_content(p) if content: remaining[p] = content log.error("=== spm_uninstall finished: FAILED. Remaining meaningful content: %s ===", remaining) print 'retcode' + str(1) + 'retcode' # dir: string -> directory where installer exist # args: string -> which includes all parameters with space delimiter def spm_update(dir, args): log.info("=== spm_update started (dir=%s, args=%s) ===", dir, args) os.chdir(dir) argslist = args.split() try: subprocess.check_call(argslist) time.sleep(60) kill_freemake_processes() log.info("Update completed successfully") print 'retcode' + str(0) + 'retcode' except subprocess.CalledProcessError as e: log.error("Update failed with return code %s", e.returncode) print 'retcode' + str(e.returncode) + 'retcode'