import os import re import ssl import time import shutil import ctypes import urllib2 import _winreg from subprocess import Popen, PIPE class disable_file_system_redirection: _disable = ctypes.windll.kernel32.Wow64DisableWow64FsRedirection _revert = ctypes.windll.kernel32.Wow64RevertWow64FsRedirection def __enter__(self): self.old_value = ctypes.c_long() self.success = self._disable(ctypes.byref(self.old_value)) def __exit__(self, type, value, traceback): if self.success: self._revert(self.old_value) APP_NAME = 'Audacity' TARGET_VER = '4.0.1.262721353' PRODUCT_CODE = '{74683A1C-A26F-4CF8-9DF9-1B80B14A43D3}' URL = "https://cdn-patchportal-one.comodo.com/portal/packages/spm/Audacity/x64/Audacity-4.0.1.262721353-x64.msi" TEMP_DIR = os.environ['TEMP'] DOWNLOAD_TO = os.path.join(TEMP_DIR, URL.split('/')[-1]) MSI_LOG = os.path.join(TEMP_DIR, 'audacity_msi.log') SPM_LOG = os.path.join(os.environ.get('SystemRoot', 'C:\\Windows'), 'Temp', 'audacity_spm.log') GUID_RE = re.compile(r'\{[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}\}') SUCCESS_CODES = (0, 1605, 3010) # 1605 = not installed, 3010 = reboot required UNINST_ROOTS = [ r'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', r'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall', ] UDATA = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData' CLASSES_PROD = r'SOFTWARE\Classes\Installer\Products' CLASSES_FEAT = r'SOFTWARE\Classes\Installer\Features' V64 = _winreg.KEY_WOW64_64KEY HKLM = _winreg.HKEY_LOCAL_MACHINE HKU = _winreg.HKEY_USERS INSTALL_DIRS = [r'C:\Program Files\Audacity 4', r'C:\Program Files\Audacity', r'C:\Program Files (x86)\Audacity 4', r'C:\Program Files (x86)\Audacity'] # ================================================================ helpers def clean(s): # remove NUL / non-printable chars (msiexec UTF-16 text truncates the ITSM log) return ''.join(c for c in str(s) if 32 <= ord(c) < 127) def log(msg): line = ' '.join(clean(msg).split())[:300] print(line) try: with open(SPM_LOG, 'a') as f: f.write(time.strftime('%Y-%m-%d %H:%M:%S ') + line + '\n') except Exception: pass def marker(code): print('retcode%sretcode' % code) def run(command, raw=False): with disable_file_system_redirection(): p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) out, err = p.communicate() text = out or err or '' if raw: return p.returncode, ''.join(c for c in str(text) if c in '\r\n\t' or 32 <= ord(c) < 127) return p.returncode, ' '.join(clean(text).split()) def refresh_shell(): try: ctypes.windll.shell32.SHChangeNotify(0x08000000, 0, None, None) except Exception: pass def download_file(dest, url): headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' '(KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'} context = ssl._create_unverified_context() try: req = urllib2.urlopen(urllib2.Request(url, headers=headers), context=context) with open(dest, 'wb') as f: while True: chunk = req.read(1024 * 1024) if not chunk: break f.write(chunk) if os.path.isfile(dest) and os.path.getsize(dest) > 0: return True, 'Downloaded %s KB' % (os.path.getsize(dest) / 1024) return False, 'Downloaded file is empty' except Exception as e: return False, 'Download failed: %s' % e def pack_guid(code): """{74683A1C-...} -> packed form used under Installer\\Products and UserData.""" g = code.strip('{}').replace('-', '').upper() rest = g[16:] swapped = ''.join(rest[i + 1] + rest[i] for i in range(0, 16, 2)) return g[0:8][::-1] + g[8:12][::-1] + g[12:16][::-1] + swapped # ================================================================ Windows Installer busy (1618) def msi_busy(): """True while another msiexec install/uninstall holds the _MSIExecute mutex.""" try: h = ctypes.windll.kernel32.OpenMutexW(0x00100000, False, u'Global\\_MSIExecute') if h: ctypes.windll.kernel32.CloseHandle(h) return True except Exception: pass return False def wait_msi_idle(timeout): end = time.time() + timeout while msi_busy(): if time.time() > end: return False time.sleep(5) return True def kill_stale_msiexec(): """Last resort: kill msiexec.exe client processes (not the /V service).""" ret, out = run('wmic process where "name=\'msiexec.exe\'" get ProcessId,CommandLine /format:csv', raw=True) for line in out.splitlines(): parts = line.strip().split(',') if len(parts) < 3 or not parts[-1].strip().isdigit(): continue pid = parts[-1].strip() cmd = ','.join(parts[1:-1]).strip().lower() if cmd.endswith('/v'): continue r, o = run('taskkill /F /PID %s' % pid) log('Killed stale msiexec pid %s (%s): %s' % (pid, cmd[:80], r)) # ================================================================ registry def _subkeys(key): i = 0 while True: try: yield _winreg.EnumKey(key, i) except WindowsError: return i += 1 def _val(key, name): try: return _winreg.QueryValueEx(key, name)[0] except WindowsError: return '' def _open(path, hive=HKLM): try: return _winreg.OpenKey(hive, path, 0, _winreg.KEY_READ | V64) except WindowsError: return None def _hive(name): return HKU if name == 'HKU' else HKLM def key_exists(h, path): k = _open(path, _hive(h)) if k: _winreg.CloseKey(k) return True return False def user_sids(): k = _open('', HKU) sids = [] if k: sids = [s for s in _subkeys(k) if not s.endswith('_Classes')] _winreg.CloseKey(k) return sids def _scan_uninstall(found, h, root): base = _open(root, _hive(h)) if not base: return for sub in list(_subkeys(base)): k = _open(root + '\\' + sub, _hive(h)) if not k: continue name = _val(k, 'DisplayName') if name and name.lower().startswith(APP_NAME.lower()): m = GUID_RE.search('%s %s %s' % (_val(k, 'UninstallString'), _val(k, 'ModifyPath'), sub)) if m: code = m.group(0).upper() found[code] = (name, str(_val(k, 'DisplayVersion')), code) _winreg.CloseKey(k) _winreg.CloseKey(base) def find_installed(hklm_only=False): """MSI based Audacity (any version): list of (name, version, product_code). hklm_only=True -> only what Control Panel can see (HKLM Uninstall).""" found = {} for root in UNINST_ROOTS: _scan_uninstall(found, 'HKLM', root) if not hklm_only: ud = _open(UDATA) if ud: for sid in list(_subkeys(ud)): prods = _open(UDATA + '\\' + sid + '\\Products') if not prods: continue for pc in list(_subkeys(prods)): k = _open(UDATA + '\\' + sid + '\\Products\\' + pc + '\\InstallProperties') if k: name = _val(k, 'DisplayName') if name and name.lower().startswith(APP_NAME.lower()): m = GUID_RE.search('%s %s' % (_val(k, 'UninstallString'), _val(k, 'ModifyPath'))) if m: code = m.group(0).upper() found[code] = (name, str(_val(k, 'DisplayVersion')), code) _winreg.CloseKey(k) _winreg.CloseKey(prods) _winreg.CloseKey(ud) for sid in user_sids(): for root in UNINST_ROOTS: _scan_uninstall(found, 'HKU', sid + '\\' + root) return list(found.values()) def find_exe_installed(): """EXE (Inno Setup etc.) based Audacity, e.g. HKLM\\...\\Uninstall\\Audacity_is1. Returns list of (root, subkey, name, version, uninstall_cmd, install_location).""" found = [] for root in UNINST_ROOTS: base = _open(root) if not base: continue for sub in list(_subkeys(base)): if GUID_RE.match(sub): continue k = _open(root + '\\' + sub) if not k: continue name = _val(k, 'DisplayName') if name and name.lower().startswith(APP_NAME.lower()) and not _val(k, 'WindowsInstaller'): cmd = _val(k, 'QuietUninstallString') or _val(k, 'UninstallString') found.append((root, sub, clean(name), clean(_val(k, 'DisplayVersion')), clean(cmd), clean(_val(k, 'InstallLocation')))) _winreg.CloseKey(k) _winreg.CloseKey(base) return found def trace_paths(code): """Every registry location Windows Installer / Uninstall may use for this product.""" packed = pack_guid(code) paths = [] for root in UNINST_ROOTS: paths.append(('HKLM', root + '\\' + code)) paths.append(('HKLM', CLASSES_PROD + '\\' + packed)) paths.append(('HKLM', CLASSES_FEAT + '\\' + packed)) ud = _open(UDATA) if ud: for sid in list(_subkeys(ud)): paths.append(('HKLM', UDATA + '\\' + sid + '\\Products\\' + packed)) _winreg.CloseKey(ud) for sid in user_sids(): base = sid + '\\Software\\Microsoft\\Installer\\' paths.append(('HKU', base + 'Products\\' + packed)) paths.append(('HKU', base + 'Features\\' + packed)) for root in UNINST_ROOTS: paths.append(('HKU', sid + '\\' + root + '\\' + code)) return paths def existing_traces(code): return [(h, p) for h, p in trace_paths(code) if key_exists(h, p)] def msi_registered(code): try: return ctypes.windll.msi.MsiQueryProductStateW(unicode(code)) != -1 except Exception: return False def is_machine_install(code): """True only if product is registered in the machine (all-users) context.""" try: buf = ctypes.create_unicode_buffer(256) size = ctypes.c_ulong(256) r = ctypes.windll.msi.MsiGetProductInfoExW(unicode(code), None, 4, u'VersionString', buf, ctypes.byref(size)) return r == 0 except Exception: return False def still_present(code): return bool(existing_traces(code)) or msi_registered(code) def any_audacity_present(): """MSI or EXE based Audacity of any version.""" if find_installed() or find_exe_installed(): return True return still_present(PRODUCT_CODE) def force_cleanup(): """Remove every registry trace (MSI + EXE entries) and the install folders.""" codes = set([PRODUCT_CODE]) for _, _, c in find_installed(): codes.add(c) for code in codes: for h, p in existing_traces(code): ret, out = run('reg delete "%s\\%s" /f /reg:64' % (h, p)) log('Delete %s\\%s -> %s' % (h, p, ret)) for root, sub, name, ver, cmd, loc in find_exe_installed(): ret, out = run('reg delete "HKLM\\%s\\%s" /f /reg:64' % (root, sub)) log('Delete EXE entry %s -> %s' % (sub, ret)) for d in INSTALL_DIRS: run('rmdir /s /q "%s"' % d) run('del /f /q "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Audacity*.lnk"') run('del /f /q "C:\\Users\\Public\\Desktop\\Audacity*.lnk"') # ================================================================ Control Panel visibility fix def _hklm_value_exists(code, name): k = _open(UNINST_ROOTS[0] + '\\' + code) if not k: return False try: _winreg.QueryValueEx(k, name) return True except WindowsError: return False finally: _winreg.CloseKey(k) def _hklm_delete_value(code, name): try: k = _winreg.OpenKey(HKLM, UNINST_ROOTS[0] + '\\' + code, 0, _winreg.KEY_SET_VALUE | V64) try: _winreg.DeleteValue(k, name) log('Deleted value %s from HKLM Uninstall\\%s' % (name, code)) except WindowsError: pass _winreg.CloseKey(k) except WindowsError: pass def cp_visible(code): """HKLM entry exists AND Control Panel will not hide it.""" if not _open(UNINST_ROOTS[0] + '\\' + code): return False if _hklm_value_exists(code, 'SystemComponent'): return False if _hklm_value_exists(code, 'WindowsInstaller') and not is_machine_install(code): return False return True def promote_to_hklm(code): """Make sure HKLM Uninstall has the entry (copy from HKU, else write it).""" dst = UNINST_ROOTS[0] + '\\' + code if not key_exists('HKLM', dst): for h, p in existing_traces(code): if h == 'HKU' and p.lower().endswith(('uninstall\\' + code).lower()): ret, out = run('reg copy "HKU\\%s" "HKLM\\%s" /s /f' % (p, dst)) log('Promote HKU -> HKLM: %s %s' % (ret, out)) break if not key_exists('HKLM', dst): try: k = _winreg.CreateKeyEx(HKLM, dst, 0, _winreg.KEY_WRITE | V64) vals = { 'DisplayName': 'Audacity 4.0', 'DisplayVersion': TARGET_VER, 'Publisher': 'Audacity', 'DisplayIcon': 'C:\\Program Files\\Audacity 4\\Audacity.exe,0', 'InstallLocation': 'C:\\Program Files\\Audacity 4\\', 'URLInfoAbout': 'https://audacityteam.org', 'UninstallString': 'MsiExec.exe /X%s' % code, } for n, v in vals.items(): _winreg.SetValueEx(k, n, 0, _winreg.REG_SZ, v) _winreg.SetValueEx(k, 'NoModify', 0, _winreg.REG_DWORD, 1) _winreg.CloseKey(k) log('Wrote HKLM Uninstall entry directly') except Exception as e: log('Write HKLM entry failed: %s' % e) # Control Panel hides MSI entries (WindowsInstaller=1) that are not registered machine-wide if _hklm_value_exists(code, 'SystemComponent'): _hklm_delete_value(code, 'SystemComponent') if _hklm_value_exists(code, 'WindowsInstaller') and not is_machine_install(code): _hklm_delete_value(code, 'WindowsInstaller') return cp_visible(code) # ================================================================ 1612 fix def fix_source(code): """Repair the product's source list so msiexec can find an MSI (fixes 1612).""" try: msi = ctypes.windll.msi pkg = unicode(os.path.basename(DOWNLOAD_TO)) src = unicode(os.path.dirname(DOWNLOAD_TO)) + u'\\' for sid, ctx in ((None, 4), (u'S-1-5-18', 2), (u'S-1-5-18', 1)): r1 = msi.MsiSourceListSetInfoW(unicode(code), sid, ctx, 1, u'PackageName', pkg) r2 = msi.MsiSourceListAddSourceExW(unicode(code), sid, ctx, 1, src, 1) log('fix_source ctx=%s: %s %s' % (ctx, r1, r2)) if r1 == 0 and r2 == 0: break except Exception as e: log('fix_source API error: %s' % e) packed = pack_guid(code) bases = [('HKLM', CLASSES_PROD)] + [('HKU', s + '\\Software\\Microsoft\\Installer\\Products') for s in user_sids()] for h, base in bases: try: k = _open(base + '\\' + packed + '\\SourceList', _hive(h)) if not k: continue pkg_name = _val(k, 'PackageName') last = _val(k, 'LastUsedSource') # e.g. n;1;C:\WINDOWS\TEMP\ _winreg.CloseKey(k) if pkg_name and last: folder = last.split(';')[-1] if folder and os.path.isdir(folder): target = os.path.join(folder, pkg_name) if not os.path.isfile(target): shutil.copyfile(DOWNLOAD_TO, target) log('Copied MSI to expected source %s' % target) except Exception as e: log('fix_source copy error: %s' % e) # ================================================================ uninstall def _uninstall_exe(entry): """Silent uninstall of the old EXE (Inno Setup) version.""" root, sub, name, ver, cmd, loc = entry keypath = root + '\\' + sub m = re.match(r'\s*"([^"]+)"', cmd) or re.match(r'\s*(\S+\.exe)', cmd, re.I) exe = m.group(1) if m else '' log('Uninstalling EXE version: %s %s (%s)' % (name, ver, exe)) if exe and os.path.isfile(exe): ret, out = run('"%s" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' % exe) log('EXE uninstall exit code: %s %s' % (ret, out)) # Inno uninstaller hands over to a temp copy - wait until it is really gone for _ in range(24): time.sleep(5) if not key_exists('HKLM', keypath) and not os.path.isfile(exe): break else: log('EXE uninstaller not found: %s' % exe) if key_exists('HKLM', keypath): ret, out = run('reg delete "HKLM\\%s" /f /reg:64' % keypath) log('Forced registry delete %s: %s' % (sub, ret)) folder = loc.rstrip('\\') or (os.path.dirname(exe) if exe else '') if folder and 'audacity' in folder.lower() and os.path.isdir(folder): run('rmdir /s /q "%s"' % folder) log('Removed folder %s' % folder) def _uninstall_one(code): ok, msg = download_file(DOWNLOAD_TO, URL) log(msg) if ok: fix_source(code) if not wait_msi_idle(120): log('Windows Installer busy before uninstall') # 1) by product code ret, out = run('msiexec.exe /x %s /qn /norestart /L*v "%s"' % (code, MSI_LOG)) log('Method1 uninstall by code: %s %s' % (ret, out)) if ret in SUCCESS_CODES or not ok: return ret # 2) by MSI file ret, out = run('msiexec.exe /x "%s" /qn /norestart /L*v "%s"' % (DOWNLOAD_TO, MSI_LOG)) log('Method2 uninstall by MSI file: %s %s' % (ret, out)) return ret def _do_uninstall(): """Remove old EXE version + any MSI version. Returns 0 on success, 1 on failure.""" run('taskkill /F /IM Audacity.exe') exe_entries = find_exe_installed() codes = set(c for _, _, c in find_installed()) if still_present(PRODUCT_CODE): codes.add(PRODUCT_CODE) if not codes and not exe_entries: log('%s not found - nothing to uninstall' % APP_NAME) return 0 for entry in exe_entries: _uninstall_exe(entry) for code in codes: log('Uninstalling MSI %s' % code) _uninstall_one(code) time.sleep(5) if any_audacity_present(): log('Normal uninstall failed - running force cleanup') force_cleanup() try: os.remove(DOWNLOAD_TO) except Exception: pass refresh_shell() if any_audacity_present(): log('UNINSTALL FAILED - still present: %s %s %s' % (find_installed(), find_exe_installed(), existing_traces(PRODUCT_CODE))) return 1 log('UNINSTALL SUCCESS') return 0 # ================================================================ install def _install_msi(): """msiexec install with wait/retry for 1618 (another installation in progress).""" ret = 1618 for attempt in range(1, 6): if not wait_msi_idle(120): log('Windows Installer still busy after wait') if attempt >= 3: kill_stale_msiexec() time.sleep(5) # ALLUSERS=1 -> machine-wide install ret, out = run('msiexec.exe /i "%s" /qn /norestart ALLUSERS=1 /L*v "%s"' % (DOWNLOAD_TO, MSI_LOG)) log('Install exit code (try %s): %s %s' % (attempt, ret, out)) if ret != 1618: break time.sleep(30) return ret def _do_install(): # target MSI version already installed and visible in Control Panel? for name, ver, code in find_installed(hklm_only=True): if ver.startswith(TARGET_VER) and cp_visible(code): log('%s %s already installed %s' % (name, ver, code)) for entry in find_exe_installed(): # remove leftover old EXE version _uninstall_exe(entry) return 0 # old EXE version / old MSI / leftover registration must go first # (otherwise msiexec treats /i as maintenance and looks for old source -> 1612) if any_audacity_present(): log('Existing Audacity found - cleaning before install') if _do_uninstall() != 0: log('INSTALL FAILED - could not remove old version') return 1 ok, msg = download_file(DOWNLOAD_TO, URL) log(msg) if not ok: log('INSTALL FAILED - download error') return 1 ret = _install_msi() try: os.remove(DOWNLOAD_TO) except Exception: pass if ret not in (0, 3010): log('INSTALL FAILED - exit code %s' % ret) return ret or 1 # make sure Control Panel can see it code = PRODUCT_CODE for n, v, c in find_installed(): if v.startswith(TARGET_VER): code = c break if not promote_to_hklm(code): log('INSTALL FAILED - Control Panel entry not visible') return 1 refresh_shell() log('INSTALL SUCCESS machine_wide=%s code=%s' % (is_machine_install(code), code)) return 0 # ================================================================ entry points def spm_uninstall(dir, args): rc = _do_uninstall() marker(rc) return rc def spm_install(dir, args): rc = _do_install() marker(rc) return rc def spm_update(dir, args): for name, ver, code in find_installed(hklm_only=True): if ver.startswith(TARGET_VER) and cp_visible(code) and not find_exe_installed(): log('Already on %s - no update needed' % TARGET_VER) marker(0) return 0 if _do_uninstall() != 0: log('Update aborted - uninstall failed') marker(1) return 1 time.sleep(10) rc = _do_install() marker(rc) return rc