import wx import os import sys import time import logging import threading import subprocess import applescript from pathlib import Path from resources.wx_gui import gui_about from resources import constants from data import model_array, os_data, smbios_data class AutoUpdateStages: INACTIVE = 0 CHECKING = 1 BUILDING = 2 INSTALLING = 3 ROOT_PATCHING = 4 FINISHED = 5 class GenerateMenubar: def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None: self.frame: wx.Frame = frame self.constants: constants.Constants = global_constants def generate(self) -> wx.MenuBar: menubar = wx.MenuBar() fileMenu = wx.Menu() aboutItem = fileMenu.Append(wx.ID_ABOUT, "&About OpenCore Legacy Patcher") fileMenu.AppendSeparator() relaunchItem = fileMenu.Append(wx.ID_ANY, "&Relaunch as Root") menubar.Append(fileMenu, "&File") self.frame.SetMenuBar(menubar) self.frame.Bind(wx.EVT_MENU, lambda event: gui_about.AboutFrame(self.constants), aboutItem) self.frame.Bind(wx.EVT_MENU, lambda event: RelaunchApplicationAsRoot(self.frame, self.constants).relaunch(None), relaunchItem) if os.geteuid() == 0: relaunchItem.Enable(False) class GaugePulseCallback: """ Uses an alternative Pulse() method for wx.Gauge() on macOS Monterey+ with non-Metal GPUs Dirty hack, however better to display some form of animation than none at all """ def __init__(self, global_constants: constants.Constants, gauge: wx.Gauge) -> None: self.gauge: wx.Gauge = gauge self.pulse_thread: threading.Thread = None self.pulse_thread_active: bool = False self.gauge_value: int = 0 self.pulse_forward: bool = True self.max_value: int = gauge.GetRange() self.non_metal_alternative: bool = CheckProperties(global_constants).host_is_non_metal() def start_pulse(self) -> None: if self.non_metal_alternative is False: self.gauge.Pulse() return self.pulse_thread_active = True self.pulse_thread = threading.Thread(target=self._pulse) self.pulse_thread.start() def stop_pulse(self) -> None: if self.non_metal_alternative is False: return self.pulse_thread_active = False self.pulse_thread.join() def _pulse(self) -> None: while self.pulse_thread_active: if self.gauge_value == 0: self.pulse_forward = True elif self.gauge_value == self.max_value: self.pulse_forward = False if self.pulse_forward: self.gauge_value += 1 else: self.gauge_value -= 1 wx.CallAfter(self.gauge.SetValue, self.gauge_value) time.sleep(0.005) class Centre: """ Alternative to wx.Frame.Centre() for macOS Ventura+ on non-Metal GPUs ---------- As reported by socamx#3874, all of their non-Metal Mac minis would incorrectly centre at the top of the screen, rather than the screen centre. Half of the window frame would be off-screen, making it rather difficult to grab frame handle and move the window. This bug is only triggered when the application is launched via AppleScript, ie. Relaunch as root for root patching. As calculating screen centre is trivial, this is safe for all non-Metal Macs. Test unit specs: - Macmini4,1 (2010, Nvidia Tesla) - Asus VP228HE 21.5" Display (1920x1080) """ def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None: self.frame: wx.Frame = frame self.constants: constants.Constants = global_constants self._centre() def _centre(self) -> None: """ Calculate centre position of screen and set window position """ if self.constants.detected_os < os_data.os_data.ventura: self.frame.Centre() return if CheckProperties(self.constants).host_is_non_metal() is False: self.frame.Centre() return # Get screen resolution screen_resolution = wx.DisplaySize() window_size = self.frame.GetSize() # Calculate window position x_pos = int((screen_resolution[0] - window_size[0]) / 2) y_pos = int((screen_resolution[1] - window_size[1]) / 2) self.frame.SetPosition((x_pos, y_pos)) class CheckProperties: def __init__(self, global_constants: constants.Constants) -> None: self.constants: constants.Constants = global_constants def host_can_build(self): """ Check if host supports building OpenCore configs """ if self.constants.custom_model: return True if self.constants.host_is_hackintosh is True: return False if self.constants.allow_oc_everywhere is True: return True if self.constants.computer.real_model in model_array.SupportedSMBIOS: return True return False def host_is_non_metal(self, general_check: bool = False): """ Check if host is non-metal Primarily for wx.Gauge().Pulse() workaround (where animation doesn't work on Monterey+) """ if self.constants.detected_os < os_data.os_data.monterey and general_check is False: return False if self.constants.detected_os < os_data.os_data.big_sur and general_check is True: return False if not Path("/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLightOld.dylib").exists(): # SkyLight stubs are only used on non-Metal return False return True def host_has_cpu_gen(self, gen: int) -> bool: """ Check if host has a CPU generation equal to or greater than the specified generation """ model = self.constants.custom_model if self.constants.custom_model else self.constants.computer.real_model if model in smbios_data.smbios_dictionary: if smbios_data.smbios_dictionary[model]["CPU Generation"] >= gen: return True return False class PayloadMount: def __init__(self, global_constants: constants.Constants, frame: wx.Frame) -> None: self.constants: constants.Constants = global_constants self.frame: wx.Frame = frame def is_unpack_finished(self): if self.constants.unpack_thread.is_alive(): return False if Path(self.constants.payload_kexts_path).exists(): return True # Raise error to end program popup = wx.MessageDialog( self.frame, f"During unpacking of our internal files, we seemed to have encountered an error.\n\nIf you keep seeing this error, please try rebooting and redownloading the application.", "Internal Error occurred!", style=wx.OK | wx.ICON_EXCLAMATION ) popup.ShowModal() self.frame.Freeze() sys.exit(1) class ThreadHandler(logging.Handler): """ Reroutes logging output to a wx.TextCtrl using UI callbacks """ def __init__(self, text_box: wx.TextCtrl): logging.Handler.__init__(self) self.text_box = text_box def emit(self, record: logging.LogRecord): wx.CallAfter(self.text_box.AppendText, self.format(record) + '\n') class RestartHost: """ Restarts the host machine """ def __init__(self, frame: wx.Frame) -> None: self.frame: wx.Frame = frame def restart(self, event: wx.Event = None, message: str = ""): self.popup = wx.MessageDialog( self.frame, message, "Reboot to apply?", wx.YES_NO | wx.YES_DEFAULT | wx.ICON_INFORMATION ) self.popup.SetYesNoLabels("Reboot", "Ignore") answer = self.popup.ShowModal() if answer == wx.ID_YES: # Reboots with Count Down prompt (user can still dismiss if needed) self.frame.Hide() wx.Yield() try: applescript.AppleScript('tell app "loginwindow" to «event aevtrrst»').run() except applescript.ScriptError as e: logging.error(f"Error while trying to reboot: {e}") sys.exit(0) class RelaunchApplicationAsRoot: """ Relaunches the application as root """ def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None: self.constants = global_constants self.frame: wx.Frame = frame def relaunch(self, event: wx.Event): self.dialog = wx.MessageDialog( self.frame, "OpenCore Legacy Patcher needs to relaunch as admin to continue. You will be prompted to enter your password.", "Relaunch as root?", wx.YES_NO | wx.ICON_QUESTION ) # Show Dialog Box if self.dialog.ShowModal() != wx.ID_YES: logging.info("User cancelled relaunch") return timer: int = 5 program_arguments: str = "" if event: if event.GetEventObject() != wx.Menu: try: if event.GetEventObject().GetLabel() in ["Start Root Patching", "Reinstall Root Patches"]: program_arguments = " --gui_patch" elif event.GetEventObject().GetLabel() == "Revert Root Patches": program_arguments = " --gui_unpatch" except TypeError: pass if self.constants.launcher_script is None: program_arguments = f"'{self.constants.launcher_binary}'{program_arguments}" else: program_arguments = f"{self.constants.launcher_binary} {self.constants.launcher_script}{program_arguments}" # Relaunch as root args = [ "osascript", "-e", f'''do shell script "{program_arguments}"''' ' with prompt "OpenCore Legacy Patcher needs administrator privileges to relaunch as admin."' " with administrator privileges" " without altering line endings", ] self.frame.DestroyChildren() self.frame.SetSize(300, 300) self.frame.Centre() # Header header = wx.StaticText(self.frame, label="Relaunching as root", pos=(-1, 5)) header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) header.Centre(wx.HORIZONTAL) # Add count down label countdown_label = wx.StaticText(self.frame, label=f"Closing old process in {timer} seconds", pos=(0, header.GetPosition().y + header.GetSize().height + 3)) countdown_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) countdown_label.Centre(wx.HORIZONTAL) # Set size of frame self.frame.SetSize((-1, countdown_label.GetPosition().y + countdown_label.GetSize().height + 40)) wx.Yield() logging.info(f"Relaunching as root with command: {program_arguments}") subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: wx.Yield() countdown_label.SetLabel(f"Closing old process in {timer} seconds") time.sleep(1) timer -= 1 if timer == 0: break sys.exit(0)