import os import subprocess import shutil import time import ctypes import _winreg as winreg DEBUG_LOG = r"C:\ProgramData\SPM_TenClips_debug.log" PROGRAM_FILES = os.environ.get("ProgramFiles", r"C:\Program Files") PROGRAM_FILES_X86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)") INSTALL_DIRS = [ os.path.join(PROGRAM_FILES, "TenClips"), os.path.join(PROGRAM_FILES_X86, "TenClips") ] PFRO_KEY_PATH = r"SYSTEM\CurrentControlSet\Control\Session Manager" PFRO_VALUE_NAME = "PendingFileRenameOperations" UNINSTALL_ROOTS = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), ] def log(stage, msg): line = "[%s] %s" % (stage, msg) print line try: f = open(DEBUG_LOG, "a") f.write(line + "\n") f.close() except: pass def run(cmd, stage="EXEC"): log(stage, "Running: %s" % cmd) try: rc = subprocess.call(cmd, shell=True) log(stage, "Return code: %d" % rc) return rc except Exception as e: log(stage, "Exception: %s" % str(e)) return -1 def kill_process(): log("KILL", "Killing TenClips.exe if running") run('taskkill /F /IM TenClips.exe >nul 2>&1', stage="KILL") def is_process_running(proc_name): try: out = subprocess.check_output('tasklist /FI "IMAGENAME eq %s"' % proc_name, shell=True) return proc_name.lower() in out.lower() except Exception: return False def find_uninstaller(): for folder in INSTALL_DIRS: uninstaller = os.path.join(folder, "uninstall.exe") if os.path.exists(uninstaller): return folder, uninstaller return None, None def uninstall_and_wait(timeout_sec=120): folder, uninstaller = find_uninstaller() if not uninstaller: log("UNINSTALL", "uninstall.exe not found in known install dirs") return True # nothing to uninstall, treat as already clean log("UNINSTALL", "Found uninstaller: %s" % uninstaller) run('"%s" /S' % uninstaller, stage="UNINSTALL") log("UNINSTALL", "Waiting for uninstall to finish (max %ds)" % timeout_sec) waited = 0 poll_interval = 3 while waited < timeout_sec: uninstaller_running = is_process_running("uninstall.exe") tenclips_running = is_process_running("TenClips.exe") if not uninstaller_running and not tenclips_running: log("UNINSTALL", "Uninstall process finished after %ds" % waited) return True time.sleep(poll_interval) waited += poll_interval log("UNINSTALL", "Timed out waiting for uninstall to finish after %ds" % timeout_sec) return False def cleanup_folders(): for folder in INSTALL_DIRS: if os.path.exists(folder): log("CLEANUP", "Removing leftover folder: %s" % folder) try: shutil.rmtree(folder, ignore_errors=True) except Exception as e: log("CLEANUP", "Failed to remove %s: %s" % (folder, str(e))) if os.path.exists(folder): log("CLEANUP", "Folder still present after rmtree (locked files?): %s" % folder) else: log("CLEANUP", "Folder removed: %s" % folder) def find_and_delete_uninstall_key(): access_flags = [0, winreg.KEY_WOW64_64KEY, winreg.KEY_WOW64_32KEY] for hive, base_path in UNINSTALL_ROOTS: for extra_flag in access_flags: try: access = winreg.KEY_READ | extra_flag base_key = winreg.OpenKey(hive, base_path, 0, access) except WindowsError: continue try: i = 0 matched_subkeys = [] while True: try: subkey_name = winreg.EnumKey(base_key, i) except WindowsError: break i += 1 try: subkey = winreg.OpenKey(base_key, subkey_name, 0, access) display_name, _ = winreg.QueryValueEx(subkey, "DisplayName") winreg.CloseKey(subkey) if display_name and "tenclips" in display_name.lower(): matched_subkeys.append(subkey_name) except WindowsError: continue for subkey_name in matched_subkeys: try: del_access = winreg.KEY_ALL_ACCESS | extra_flag winreg.DeleteKeyEx(base_key, subkey_name, del_access, 0) log("CLEANUP", "Deleted registry key: %s\\%s" % (base_path, subkey_name)) except AttributeError: try: winreg.DeleteKey(base_key, subkey_name) log("CLEANUP", "Deleted registry key: %s\\%s" % (base_path, subkey_name)) except Exception as e: log("CLEANUP", "Failed to delete key %s: %s" % (subkey_name, str(e))) except Exception as e: log("CLEANUP", "Failed to delete key %s: %s" % (subkey_name, str(e))) finally: winreg.CloseKey(base_key) def _strip_prefix(raw_path): if raw_path.startswith(r"\??\ "[:-1]): return raw_path[4:] return raw_path def _is_file_locked(path): if not path or not os.path.exists(path): return False try: handle = ctypes.windll.kernel32.CreateFileW( unicode(path), 0xC0000000, 0, None, 3, 0, None ) if handle == -1 or handle is None: return True ctypes.windll.kernel32.CloseHandle(handle) return False except Exception: return True def check_reboot_pending(): """ Best-effort PendingFileRenameOperations clear (no-reboot path), then final decision: True if reboot is genuinely still required. """ reboot_flags = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired"), ] for hive, path in reboot_flags: try: key = winreg.OpenKey(hive, path, 0, winreg.KEY_READ) winreg.CloseKey(key) log("REBOOT_CHECK", "Hard reboot-pending flag present: %s" % path) return True except WindowsError: continue try: key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, PFRO_KEY_PATH, 0, winreg.KEY_READ) val, _ = winreg.QueryValueEx(key, PFRO_VALUE_NAME) winreg.CloseKey(key) except WindowsError: log("REBOOT_CHECK", "No PendingFileRenameOperations - no reboot needed") return False if not val: return False entries = list(val) pairs = [] i = 0 while i < len(entries): source = entries[i] dest = entries[i + 1] if i + 1 < len(entries) else "" pairs.append((source, dest)) i += 2 remaining = [] for source_raw, dest_raw in pairs: source_path = _strip_prefix(source_raw) dest_path = _strip_prefix(dest_raw) if dest_raw else "" if not os.path.exists(source_path): continue if _is_file_locked(source_path): log("REBOOT_CHECK", "Locked file still pending: %s" % source_path) remaining.append(source_raw) remaining.append(dest_raw) continue try: if not dest_path: os.remove(source_path) else: if os.path.exists(dest_path): os.remove(dest_path) os.rename(source_path, dest_path) log("REBOOT_CHECK", "Cleared pending op now (no reboot needed for this file): %s" % source_path) except Exception as e: log("REBOOT_CHECK", "Could not clear now, leaving pending: %s (%s)" % (source_path, str(e))) remaining.append(source_raw) remaining.append(dest_raw) try: rkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, PFRO_KEY_PATH, 0, winreg.KEY_SET_VALUE) if remaining: winreg.SetValueEx(rkey, PFRO_VALUE_NAME, 0, winreg.REG_MULTI_SZ, remaining) else: try: winreg.DeleteValue(rkey, PFRO_VALUE_NAME) except WindowsError: pass winreg.CloseKey(rkey) except Exception as e: log("REBOOT_CHECK", "Failed to rewrite PFRO key: %s" % str(e)) if remaining: log("REBOOT_CHECK", "%d file(s) still locked - reboot required" % (len(remaining) // 2)) return True log("REBOOT_CHECK", "All pending ops cleared without reboot") return False def spm_install(dir, args): os.chdir(dir) argslist = args.split() try: subprocess.check_call(argslist) print 'retcode0retcode' except subprocess.CalledProcessError as e: print 'retcode' + str(e.returncode) + 'retcode' def spm_uninstall(dir, args): kill_process() finished = uninstall_and_wait() cleanup_folders() find_and_delete_uninstall_key() if not finished: log("UNINSTALL", "Uninstall did not finish cleanly within timeout") print 'retcode1retcode' return print 'retcode0retcode' def spm_update(dir, args): """ Flow: kill -> uninstall -> wait -> cleanup folder -> cleanup registry key -> check reboot pending if required: report reboot required to ITSM (retcode3010retcode) else: install TenClipsSetup32.exe /S """ kill_process() finished = uninstall_and_wait() cleanup_folders() find_and_delete_uninstall_key() if not finished: log("UPDATE", "Uninstall did not finish cleanly within timeout") print 'retcode1retcode' return reboot_required = check_reboot_pending() if reboot_required: log("UPDATE", "Reboot required - reporting to ITSM, install deferred") print 'retcode3010retcode' return log("UPDATE", "No reboot required - proceeding with install") os.chdir(dir) argslist = args.split() try: subprocess.check_call(argslist) print 'retcode0retcode' except subprocess.CalledProcessError as e: print 'retcode' + str(e.returncode) + 'retcode'