import wx import os import sys import time import logging import plistlib import traceback import threading import subprocess from pathlib import Path from resources import ( constants, kdk_handler, ) from resources.sys_patch import ( sys_patch, sys_patch_detect ) from resources.wx_gui import ( gui_main_menu, gui_support, gui_download, ) from data import os_data class SysPatchFrame(wx.Frame): """ Create a frame for root patching Uses a Modal Dialog for smoother transition from other frames """ def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None, patches: dict = {}): super(SysPatchFrame, self).__init__(parent, title=title, size=(350, 260), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) self.title = title self.constants: constants.Constants = global_constants self.frame_modal: wx.Dialog = None self.return_button: wx.Button = None self.available_patches: bool = False self.frame_modal = wx.Dialog(self, title=title, size=(360, 200)) self.SetPosition(screen_location) if screen_location else self.Centre() if patches: return self._generate_elements_display_patches(self.frame_modal, patches) self.frame_modal.ShowWindowModal() if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE: if self.available_patches is False: gui_support.RestartHost(self).restart(message="No root patch updates needed!\n\nWould you like to reboot to apply the new OpenCore build?") def _kdk_download(self, frame: wx.Frame = None) -> bool: frame = self if not frame else frame logging.info("KDK missing, generating KDK download frame") header = wx.StaticText(frame, label="Downloading Kernel Debug Kit", pos=(-1,5)) header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) header.Center(wx.HORIZONTAL) subheader = wx.StaticText(frame, label="Fetching KDK database...", pos=(-1, header.GetPosition()[1] + header.GetSize()[1] + 5)) subheader.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) subheader.Center(wx.HORIZONTAL) progress_bar = wx.Gauge(frame, range=100, pos=(-1, subheader.GetPosition()[1] + subheader.GetSize()[1] + 5), size=(250, 20)) progress_bar.Center(wx.HORIZONTAL) progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar) progress_bar_animation.start_pulse() # Set size of frame frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 35)) frame.Show() # Generate KDK object self.kdk_obj: kdk_handler.KernelDebugKitObject = None def _kdk_thread_spawn(): self.kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version) kdk_thread = threading.Thread(target=_kdk_thread_spawn) kdk_thread.start() while kdk_thread.is_alive(): wx.GetApp().Yield() if self.kdk_obj.success is False: progress_bar_animation.stop_pulse() progress_bar.SetValue(0) wx.MessageBox(f"KDK download failed: {self.kdk_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR) return False kdk_download_obj = self.kdk_obj.retrieve_download() if not kdk_download_obj: # KDK is already downloaded return True gui_download.DownloadFrame( self, title=self.title, global_constants=self.constants, download_obj=kdk_download_obj, item_name=f"KDK Build {self.kdk_obj.kdk_url_build}" ) if kdk_download_obj.download_complete is False: return False header.SetLabel(f"Validating KDK: {self.kdk_obj.kdk_url_build}") header.Center(wx.HORIZONTAL) subheader.SetLabel("Checking if checksum is valid...") subheader.Center(wx.HORIZONTAL) wx.GetApp().Yield() progress_bar_animation.stop_pulse() if self.kdk_obj.validate_kdk_checksum() is False: progress_bar.SetValue(0) logging.error("KDK checksum validation failed") logging.error(self.kdk_obj.error_msg) msg = wx.MessageDialog(frame, f"KDK checksum validation failed: {self.kdk_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR) msg.ShowModal() return False progress_bar.SetValue(100) logging.info("KDK download complete") return True def _generate_elements_display_patches(self, frame: wx.Frame = None, patches: dict = {}) -> None: """ Generate UI elements for root patching frame Format: - Title label: Post-Install Menu - Label: Available patches: - Labels: {patch name} - Button: Start Root Patching - Button: Revert Root Patches - Button: Return to Main Menu """ frame = self if not frame else frame title_label = wx.StaticText(frame, label="Post-Install Menu", pos=(-1, 10)) title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) title_label.Center(wx.HORIZONTAL) # Label: Available patches: available_label = wx.StaticText(frame, label="Available patches for your system:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 10)) available_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) available_label.Center(wx.HORIZONTAL) # Labels: {patch name} patches: dict = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() if not patches else patches can_unpatch: bool = patches["Validation: Unpatching Possible"] if not any(not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True for patch in patches): logging.info("- No applicable patches available") patches = [] # Check if OCLP has already applied the same patches no_new_patches = not self._check_if_new_patches_needed(patches) if patches else False if not patches: # Prompt user with no patches found patch_label = wx.StaticText(frame, label="No patches required", pos=(-1, available_label.GetPosition()[1] + 20)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) else: # Add Label for each patch i = 0 if no_new_patches is True: patch_label = wx.StaticText(frame, label="All applicable patches already installed", pos=(-1, available_label.GetPosition()[1] + 20)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) i = i + 20 else: longest_patch = "" for patch in patches: if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): if len(patch) > len(longest_patch): longest_patch = patch anchor = wx.StaticText(frame, label=longest_patch, pos=(-1, available_label.GetPosition()[1] + 20)) anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) anchor.Center(wx.HORIZONTAL) anchor.Hide() for patch in patches: if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): i = i + 20 logging.info(f"- Adding patch: {patch} - {patches[patch]}") patch_label = wx.StaticText(frame, label=f"- {patch}", pos=(anchor.GetPosition()[0], available_label.GetPosition()[1] + i)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) if i == 20: patch_label.SetLabel(patch_label.GetLabel().replace("-", "")) patch_label.Center(wx.HORIZONTAL) if patches["Validation: Patching Possible"] is False: # Cannot patch due to the following reasons: patch_label = wx.StaticText(frame, label="Cannot patch due to the following reasons:", pos=(-1, patch_label.GetPosition().y + 25)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) for patch in patches: if not patch.startswith("Validation"): continue if patches[patch] is False: continue if patch == "Validation: Unpatching Possible": continue patch_label = wx.StaticText(frame, label=f"- {patch.split('Validation: ')[1]}", pos=(available_label.GetPosition().x - 10, patch_label.GetPosition().y + 20)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) else: if self.constants.computer.oclp_sys_version and self.constants.computer.oclp_sys_date: date = self.constants.computer.oclp_sys_date.split(" @") date = date[0] if len(date) == 2 else "" patch_text = f"{self.constants.computer.oclp_sys_version}, {date}" patch_label = wx.StaticText(frame, label="Root Volume last patched:", pos=(-1, patch_label.GetPosition().y + 25)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) patch_label = wx.StaticText(frame, label=patch_text, pos=(available_label.GetPosition().x - 10, patch_label.GetPosition().y + 20)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) # Button: Start Root Patching start_button = wx.Button(frame, label="Start Root Patching", pos=(10, patch_label.GetPosition().y + 25), size=(170, 30)) start_button.Bind(wx.EVT_BUTTON, lambda event: self.start_root_patching(frame, patches, no_new_patches)) start_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) start_button.Center(wx.HORIZONTAL) # Button: Revert Root Patches revert_button = wx.Button(frame, label="Revert Root Patches", pos=(10, start_button.GetPosition().y + start_button.GetSize().height - 5), size=(170, 30)) revert_button.Bind(wx.EVT_BUTTON, lambda event: self.revert_root_patching(frame, patches, can_unpatch)) revert_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) revert_button.Center(wx.HORIZONTAL) # Button: Return to Main Menu return_button = wx.Button(frame, label="Return to Main Menu", pos=(10, revert_button.GetPosition().y + revert_button.GetSize().height), size=(150, 30)) return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu) return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) return_button.Center(wx.HORIZONTAL) self.return_button = return_button # Disable buttons if unsupported if not patches: start_button.Disable() else: self.available_patches = True if patches["Validation: Patching Possible"] is False: start_button.Disable() elif no_new_patches is False: start_button.SetDefault() if can_unpatch is False: revert_button.Disable() # Relaunch as root if not root uid = os.geteuid() if uid != 0: start_button.Bind(wx.EVT_BUTTON, gui_support.RelaunchApplicationAsRoot(frame, self.constants).relaunch) revert_button.Bind(wx.EVT_BUTTON, gui_support.RelaunchApplicationAsRoot(frame, self.constants).relaunch) # Set frame size frame.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 35)) def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"): """ Create UI for root patching/unpatching """ supported_variants = ["Root Patching", "Revert Root Patches"] if variant not in supported_variants: logging.error(f"Unsupported variant: {variant}") return self.frame_modal.Close() dialog = wx.Dialog(self, title=self.title, size=(400, 200)) # Title title = wx.StaticText(dialog, label=variant, pos=(-1, 10)) title.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) title.Center(wx.HORIZONTAL) if variant == "Root Patching": # Label label = wx.StaticText(dialog, label="Root Patching will patch the following:", pos=(-1, title.GetPosition()[1] + 30)) label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) label.Center(wx.HORIZONTAL) # Get longest patch label, then create anchor for patch labels longest_patch = "" for patch in patches: if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): if len(patch) > len(longest_patch): longest_patch = patch anchor = wx.StaticText(dialog, label=longest_patch, pos=(label.GetPosition()[0], label.GetPosition()[1] + 20)) anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) anchor.Center(wx.HORIZONTAL) anchor.Hide() # Labels i = 0 for patch in patches: if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): logging.info(f"- Adding patch: {patch} - {patches[patch]}") patch_label = wx.StaticText(dialog, label=f"- {patch}", pos=(anchor.GetPosition()[0], label.GetPosition()[1] + 20 + i)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) i = i + 20 if i == 20: patch_label.SetLabel(patch_label.GetLabel().replace("-", "")) patch_label.Center(wx.HORIZONTAL) elif i == 0: patch_label = wx.StaticText(dialog, label="No patches to apply", pos=(label.GetPosition()[0], label.GetPosition()[1] + 20)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) else: patch_label = wx.StaticText(dialog, label="Reverting to last sealed snapshot", pos=(-1, title.GetPosition()[1] + 30)) patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) patch_label.Center(wx.HORIZONTAL) # Text box text_box = wx.TextCtrl(dialog, pos=(10, patch_label.GetPosition()[1] + 30), size=(400, 400), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2) text_box.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) text_box.Center(wx.HORIZONTAL) self.text_box = text_box # Button: Return to Main Menu return_button = wx.Button(dialog, label="Return to Main Menu", pos=(10, text_box.GetPosition()[1] + text_box.GetSize()[1] + 5), size=(150, 30)) return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu) return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) return_button.Center(wx.HORIZONTAL) self.return_button = return_button # Set frame size dialog.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 33)) dialog.ShowWindowModal() def start_root_patching(self, patches: dict): logging.info("Starting root patching") while gui_support.PayloadMount(self.constants, self).is_unpack_finished() is False: wx.Yield() if patches["Settings: Kernel Debug Kit missing"] is True: if self._kdk_download(self) is False: self.on_return_to_main_menu() return self._generate_modal(patches, "Root Patching") thread = threading.Thread(target=self._start_root_patching, args=(patches,)) thread.start() while thread.is_alive(): wx.GetApp().Yield() self._post_patch() self.return_button.Enable() def _start_root_patching(self, patches: dict): logger = logging.getLogger() logger.addHandler(gui_support.ThreadHandler(self.text_box)) try: sys_patch.PatchSysVolume(self.constants.computer.real_model, self.constants, patches).start_patch() except: logging.error("- An internal error occurred while running the Root Patcher:\n") logging.error(traceback.format_exc()) logger.removeHandler(logger.handlers[2]) def revert_root_patching(self, patches: dict): logging.info("Reverting root patches") self._generate_modal(patches, "Revert Root Patches") thread = threading.Thread(target=self._revert_root_patching, args=(patches,)) thread.start() while thread.is_alive(): wx.GetApp().Yield() self._post_patch() self.return_button.Enable() def _revert_root_patching(self, patches: dict): logger = logging.getLogger() logger.addHandler(gui_support.ThreadHandler(self.text_box)) try: sys_patch.PatchSysVolume(self.constants.computer.real_model, self.constants, patches).start_unpatch() except: logging.error("- An internal error occurred while running the Root Patcher:\n") logging.error(traceback.format_exc()) logger.removeHandler(logger.handlers[2]) def on_return_to_main_menu(self, event: wx.Event = None): self.frame_modal.Hide() main_menu_frame = gui_main_menu.MainFrame( None, title=self.title, global_constants=self.constants, screen_location=self.GetScreenPosition() ) main_menu_frame.Show() self.frame_modal.Destroy() self.Destroy() def _post_patch(self): if self.constants.root_patcher_succeeded is False: return if self.constants.needs_to_open_preferences is False: gui_support.RestartHost(self).restart(message="Root Patcher finished successfully!\n\nWould you like to reboot now?") return if self.constants.detected_os >= os_data.os_data.ventura: gui_support.RestartHost(self).restart(message="Root Patcher finished successfully!\nIf you were prompted to open System Settings to authorize new kexts, this can be ignored. Your system is ready once restarted.\n\nWould you like to reboot now?") return # Create dialog box to open System Preferences -> Security and Privacy self.popup = wx.MessageDialog( self.frame_modal, "We just finished installing the patches to your Root Volume!\n\nHowever, Apple requires users to manually approve the kernel extensions installed before they can be used next reboot.\n\nWould you like to open System Preferences?", "Open System Preferences?", wx.YES_NO | wx.ICON_INFORMATION ) self.popup.SetYesNoLabels("Open System Preferences", "Ignore") answer = self.popup.ShowModal() if answer == wx.ID_YES: output =subprocess.run( [ "osascript", "-e", 'tell app "System Preferences" to activate', "-e", 'tell app "System Preferences" to reveal anchor "General" of pane id "com.apple.preference.security"', ], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if output.returncode != 0: # Some form of fallback if unaccelerated state errors out subprocess.run(["open", "-a", "System Preferences"]) time.sleep(5) sys.exit(0) def _check_if_new_patches_needed(self, patches: dict) -> bool: """ Checks if any new patches are needed for the user to install Newer users will assume the root patch menu will present missing patches. Thus we'll need to see if the exact same OCLP build was used already """ if self.constants.commit_info[0] in ["Running from source", "Built from source"]: return True if self.constants.computer.oclp_sys_url != self.constants.commit_info[2]: # If commits are different, assume patches are as well return True oclp_plist = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist" if not Path(oclp_plist).exists(): # If it doesn't exist, no patches were ever installed # ie. all patches applicable return True oclp_plist_data = plistlib.load(open(oclp_plist, "rb")) for patch in patches: if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True): # Patches should share the same name as the plist key # See sys_patch_dict.py for more info patch_installed = False for key in oclp_plist_data: if "Display Name" not in oclp_plist_data[key]: continue if oclp_plist_data[key]["Display Name"] == patch: patch_installed = True break if patch_installed is False: logging.info(f"- Patch {patch} not installed") return True logging.info("- No new patches detected for system") return False