Files
OpenCore-Legacy-Patcher/resources/wx_gui/gui_support.py
2023-05-30 14:18:07 -06:00

334 lines
11 KiB
Python

import wx
import os
import sys
import time
import logging
import plistlib
import threading
import subprocess
import applescript
import packaging.version
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")
fileMenu.AppendSeparator()
revealLogItem = fileMenu.Append(wx.ID_ANY, "&Reveal Log File")
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)
self.frame.Bind(wx.EVT_MENU, lambda event: subprocess.run(["open", "-R", self.constants.log_filepath]), revealLogItem)
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
Note: This work-around is no longer needed on hosts using PatcherSupportPkg 1.1.2 or newer
"""
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()
if self.non_metal_alternative is True:
if CheckProperties(global_constants).host_psp_version() >= packaging.version.Version("1.1.2"):
self.non_metal_alternative = False
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 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
def host_psp_version(self) -> packaging.version.Version:
"""
Grab PatcherSupportPkg version from OpenCore-Legacy-Patcher.plist
"""
oclp_plist_path = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist"
if not Path(oclp_plist_path).exists():
return packaging.version.Version("0.0.0")
oclp_plist = plistlib.load(open(oclp_plist_path, "rb"))
if "PatcherSupportPkg" not in oclp_plist:
return packaging.version.Version("0.0.0")
if oclp_plist["PatcherSupportPkg"].startswith("v"):
oclp_plist["PatcherSupportPkg"] = oclp_plist["PatcherSupportPkg"][1:]
return packaging.version.parse(oclp_plist["PatcherSupportPkg"])
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)