import os import subprocess # dir: string -> directory where installer exist # args: string -> which includes all parameters with space delimiter def spm_install(dir, args): os.chdir(dir) argslist = args.split() try: subprocess.check_call(argslist) try: tasklist=os.popen('Taskkill /IM "FreemakeVC.exe" /F').read() except: pass print 'retcode' + str(0) + 'retcode' except subprocess.CalledProcessError as e: 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): # unins000.exe pops a custom "Get Sale" nagware dialog (Uninstall / # Cancel) the moment it launches. It is NOT a standard Inno Setup # wizard page, so /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- does # not suppress it -- the process just sits there waiting for a click, # ITarian times the operation out, exit code 1. Manual uninstall only # "works" because a human clicks Uninstall on that dialog. # # Fix (same concept as QNAP Qfinder Pro): don't trust the vendor # uninstaller's exit code or liveness at all. Best-effort launch it, # kill it if it's still alive after a short grace window (stuck on the # dialog), then force-remove install folder(s) + registry key + # shortcuts, and verify via registry + folder check before declaring # success. import re import time import shutil import _winreg product_name = "Freemake Video Converter" uninstallkey32 = 'SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall' uninstallkey64 = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall' reg_list = [ (_winreg.HKEY_LOCAL_MACHINE, uninstallkey32, _winreg.KEY_WOW64_32KEY | _winreg.KEY_ALL_ACCESS), (_winreg.HKEY_LOCAL_MACHINE, uninstallkey64, _winreg.KEY_WOW64_64KEY | _winreg.KEY_ALL_ACCESS), ] # parent folder, not just the "Freemake Video Converter" subfolder -- # deleting only the subfolder leaves an empty (or not-so-empty) # "Freemake" folder behind in Program Files forever fallback_install_dirs = [ r"C:\Program Files (x86)\Freemake", r"C:\Program Files\Freemake", ] shortcut_paths = [ os.path.expandvars(r"%ProgramData%\Microsoft\Windows\Start Menu\Programs\Freemake"), os.path.expandvars(r"%PUBLIC%\Desktop\Freemake Video Converter.lnk"), os.path.expandvars(r"%USERPROFILE%\Desktop\Freemake Video Converter.lnk"), ] def kill_matching_processes(patterns): # taskkill's /FI "IMAGENAME eq ..." wildcard filter does NOT # reliably match a pattern with both a prefix and a literal # suffix (e.g. "_iu*.tmp") -- it just silently matches nothing, # no error, no feedback. That's why the _iuXXXXX.tmp copy kept # surviving every kill_freemake() call. Parse `tasklist` output # ourselves with fnmatch and kill by PID instead -- reliable # regardless of what taskkill's filter engine does or doesn't # support. import fnmatch import csv import StringIO try: output = os.popen('tasklist /FO CSV /NH').read() except: return try: reader = csv.reader(StringIO.StringIO(output)) except: return for row in reader: if len(row) < 2: continue image_name = row[0] pid = row[1] for pat in patterns: if fnmatch.fnmatch(image_name.lower(), pat.lower()): try: os.popen('taskkill /F /PID %s' % pid).read() except: pass break def kill_freemake(): kill_matching_processes([ "FreemakeVC.exe", "unins*.exe", "_iu*.tmp", "is-*.tmp", ]) # keep the direct-name attempts too as a cheap belt-and-suspenders try: os.popen('Taskkill /IM "FreemakeVC.exe" /F').read() except: pass try: os.popen('Taskkill /IM "unins000.exe" /F').read() except: pass try: os.popen('Taskkill /IM "unins001.exe" /F').read() except: pass try: os.popen('Taskkill /F /FI "WINDOWTITLE eq Freemake Video Converter Uninstall*"').read() except: pass def clean_temp_leftovers(): # after killing the _iuXXXXX.tmp copy, the file itself is often # left behind in Temp (Inno doesn't always self-delete cleanly if # it was force-killed mid-run) -- sweep both the per-user and # system Temp folders so they don't pile up run after run temp_dirs = [ os.path.expandvars(r"%TEMP%"), os.path.expandvars(r"%WINDIR%\Temp"), ] for td in temp_dirs: try: if not os.path.isdir(td): continue for name in os.listdir(td): lname = name.lower() if lname.startswith("_iu") and lname.endswith(".tmp"): full = os.path.join(td, name) try: os.chmod(full, 0o777) os.remove(full) except: pass except: pass def find_entries(): found = [] for reg_key, sub_key, access in reg_list: try: reg = _winreg.OpenKey(reg_key, sub_key, 0, access) except: continue i = 0 while True: try: key_value = _winreg.EnumKey(reg, i) except: break path = os.path.join(sub_key, key_value) try: Hkey = _winreg.OpenKey(reg_key, path, 0, access) dis_name, _ = _winreg.QueryValueEx(Hkey, 'DisplayName') if product_name.lower() in dis_name.strip().lower(): try: uninstr, _ = _winreg.QueryValueEx(Hkey, 'UninstallString') except: uninstr = None try: instloc, _ = _winreg.QueryValueEx(Hkey, 'InstallLocation') except: instloc = None found.append([dis_name.strip(), path, reg_key, access, uninstr, instloc]) except: pass i += 1 return found def _clear_readonly(func, path, exc_info): # onerror handler for rmtree: strip read-only attribute and retry # the failed operation once, instead of just giving up on it try: os.chmod(path, 0o777) func(path) except: pass def robust_rmtree(path, max_attempts=5): # single ignore_errors=True call swallows failures silently and # leaves the folder behind (that's what happened last run) -- so # retry with kill + attrib clear in between instead of one shot for attempt in range(max_attempts): if not os.path.isdir(path): return True kill_freemake() clean_temp_leftovers() time.sleep(1.5) # let Windows actually release the handle try: shutil.rmtree(path, onerror=_clear_readonly) except: pass if not os.path.isdir(path): return True # stubborn leftovers (locked handle, long path, etc.) - use # the OS-level fallback as a last resort per attempt try: os.system('rmdir /s /q "%s"' % path) except: pass if not os.path.isdir(path): return True time.sleep(2) return not os.path.isdir(path) def remove_shortcuts(): for path in shortcut_paths: try: if os.path.isdir(path): robust_rmtree(path) elif os.path.isfile(path): os.chmod(path, 0o777) os.remove(path) except: pass def clear_fallback_dirs(): ok = True for d in fallback_install_dirs: if os.path.isdir(d): if not robust_rmtree(d): ok = False return ok def force_remove(entry): dis_name, path, reg_key, access, uninstr, instloc = entry kill_freemake() dirs_to_clear = list(fallback_install_dirs) if instloc and instloc not in dirs_to_clear: dirs_to_clear.append(instloc) for d in dirs_to_clear: if d and os.path.isdir(d): robust_rmtree(d) remove_shortcuts() try: _winreg.DeleteKey(reg_key, path) except: pass kill_freemake() entries = find_entries() if not entries: # nothing registered - clear stray folders/shortcuts just in case clear_fallback_dirs() remove_shortcuts() print 'retcode' + str(0) + 'retcode' return for dis_name, path, reg_key, access, uninstr, instloc in entries: exe_path = None if uninstr: m = re.match(r'^\s*"([^"]+)"(.*)$', uninstr) if m: exe_path = m.group(1) else: exe_path = uninstr.strip().split(' ', 1)[0] if exe_path and os.path.isfile(exe_path): cmd = [exe_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-"] try: # Popen, NOT check_call -- unins000.exe blocks on its own # "Get Sale" nag dialog, so its exit code / liveness can't # be trusted as a success signal. proc = subprocess.Popen(cmd) waited = 0 while proc.poll() is None and waited < 8: time.sleep(1) waited += 1 if proc.poll() is None: # still alive after grace window -> stuck on the dialog kill_freemake() except Exception as e: print 'launch_error:' + str(e) kill_freemake() removed = False attempt = 0 while attempt < 6: time.sleep(3) kill_freemake() if not find_entries(): removed = True break attempt += 1 if not removed: remaining = find_entries() for entry in remaining: force_remove(entry) time.sleep(2) removed = not find_entries() # final folder/shortcut cleanup regardless of registry outcome above clear_fallback_dirs() remove_shortcuts() folders_clear = not any(os.path.isdir(d) for d in fallback_install_dirs) if removed and folders_clear: print 'retcode' + str(0) + 'retcode' else: 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): os.chdir(dir) argslist = args.split() try: subprocess.check_call(argslist) try: tasklist=os.popen('Taskkill /IM "FreemakeVC.exe" /F').read() except: pass print 'retcode' + str(0) + 'retcode' except subprocess.CalledProcessError as e: print 'retcode' + str(e.returncode) + 'retcode'