From 2e964ba9c21bdc353c4b34ae4aba0c2e2d824117 Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Sat, 13 May 2023 18:19:57 -0600 Subject: [PATCH] GUI: Add app update checks --- resources/constants.py | 2 +- resources/sys_patch/sys_patch_auto.py | 99 ++++----- resources/updates.py | 9 + resources/wx_gui/gui_download.py | 2 +- resources/wx_gui/gui_entry.py | 4 +- .../wx_gui/gui_macos_installer_download.py | 1 - resources/wx_gui/gui_main_menu.py | 76 ++++++- resources/wx_gui/gui_sys_patch.py | 1 - resources/wx_gui/gui_update.py | 205 ++++++++++++++++++ 9 files changed, 341 insertions(+), 58 deletions(-) create mode 100644 resources/wx_gui/gui_update.py diff --git a/resources/constants.py b/resources/constants.py index 841d806a0..08236cbb3 100644 --- a/resources/constants.py +++ b/resources/constants.py @@ -12,7 +12,7 @@ from data import os_data class Constants: def __init__(self) -> None: # Patcher Versioning - self.patcher_version: str = "0.6.6" # OpenCore-Legacy-Patcher + self.patcher_version: str = "0.6.5" # OpenCore-Legacy-Patcher self.patcher_support_pkg_version: str = "1.0.0" # PatcherSupportPkg self.copyright_date: str = "Copyright © 2020-2023 Dortania" self.patcher_name: str = "OpenCore Legacy Patcher" diff --git a/resources/sys_patch/sys_patch_auto.py b/resources/sys_patch/sys_patch_auto.py index 3645242cd..0c88ea48e 100644 --- a/resources/sys_patch/sys_patch_auto.py +++ b/resources/sys_patch/sys_patch_auto.py @@ -5,6 +5,7 @@ import subprocess import webbrowser import logging from pathlib import Path +import wx from resources import utilities, updates, global_settings, network_handler, constants from resources.sys_patch import sys_patch_detect @@ -42,6 +43,26 @@ class AutomaticSysPatch: logging.info("- Auto Patch option is not supported on TUI, please use GUI") return + dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates() + if dict: + for key in dict: + version = dict[key]["Version"] + logging.info(f"- Found new version: {version}") + + app = wx.App() + frame = wx.Frame(None, -1, "OpenCore Legacy Patcher") + dialog = wx.MessageDialog( + parent=frame, + message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?", + caption="Update Available for OpenCore Legacy Patcher!", + style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION + ) + dialog.SetYesNoCancelLabels("Download and install", "Always Ignore", "Ignore Once") + response = dialog.ShowModal() + if response == wx.ID_YES: + gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.UPDATE_APP) + return + if utilities.check_seal() is True: logging.info("- Detected Snapshot seal intact, detecting patches") patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() @@ -58,69 +79,43 @@ class AutomaticSysPatch: for patch in patches: if patches[patch] is True and not patch.startswith("Settings") and not patch.startswith("Validation"): patch_string += f"- {patch}\n" - # Check for updates - dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates() - if not dict: - logging.info("- No new binaries found on Github, proceeding with patching") - if self.constants.launcher_script is None: - args_string = f"'{self.constants.launcher_binary}' --gui_patch" - else: - args_string = f"{self.constants.launcher_binary} {self.constants.launcher_script} --gui_patch" - warning_str = "" - if network_handler.NetworkUtilities("https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest").verify_network_connection() is False: - warning_str = f"""\n\nWARNING: We're unable to verify whether there are any new releases of OpenCore Legacy Patcher on Github. Be aware that you may be using an outdated version for this OS. If you're unsure, verify on Github that OpenCore Legacy Patcher {self.constants.patcher_version} is the latest official release""" - - args = [ - "osascript", - "-e", - f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nmacOS wipes all root patches during OS installs and updates, so they need to be reinstalled.\n\nFollowing Patches have been detected for your system: \n{patch_string}\nWould you like to apply these patches?{warning_str}" """ - f'with icon POSIX file "{self.constants.app_icon_path}"', - ] - output = subprocess.run( - args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) - if output.returncode == 0: - args = [ - "osascript", - "-e", - f'''do shell script "{args_string}"''' - f' with prompt "OpenCore Legacy Patcher would like to patch your root volume"' - " with administrator privileges" - " without altering line endings" - ] - subprocess.run( - args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) - return + logging.info("- No new binaries found on Github, proceeding with patching") + if self.constants.launcher_script is None: + args_string = f"'{self.constants.launcher_binary}' --gui_patch" else: - for key in dict: - version = dict[key]["Version"] - github_link = dict[key]["Github Link"] - logging.info(f"- Found new version: {version}") + args_string = f"{self.constants.launcher_binary} {self.constants.launcher_script} --gui_patch" - # launch osascript to ask user if they want to apply the update - # if yes, open the link in the default browser - # we never want to run the root patcher if there are updates available + warning_str = "" + if network_handler.NetworkUtilities("https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest").verify_network_connection() is False: + warning_str = f"""\n\nWARNING: We're unable to verify whether there are any new releases of OpenCore Legacy Patcher on Github. Be aware that you may be using an outdated version for this OS. If you're unsure, verify on Github that OpenCore Legacy Patcher {self.constants.patcher_version} is the latest official release""" + + args = [ + "osascript", + "-e", + f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nmacOS wipes all root patches during OS installs and updates, so they need to be reinstalled.\n\nFollowing Patches have been detected for your system: \n{patch_string}\nWould you like to apply these patches?{warning_str}" """ + f'with icon POSIX file "{self.constants.app_icon_path}"', + ] + output = subprocess.run( + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + if output.returncode == 0: args = [ "osascript", "-e", - f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nHowever we've detected a new version of OCLP on Github. Would you like to view this?\n\nCurrent Version: {self.constants.patcher_version}\nLatest Version: {version}\n\nNote: After downloading the latest OCLP version, open the app and run the 'Post Install Root Patcher' from the main menu." """ - f'with icon POSIX file "{self.constants.app_icon_path}"', + f'''do shell script "{args_string}"''' + f' with prompt "OpenCore Legacy Patcher would like to patch your root volume"' + " with administrator privileges" + " without altering line endings" ] - output = subprocess.run( + subprocess.run( args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) - if output.returncode == 0: - webbrowser.open(github_link) - - return + return else: logging.info("- No patches detected") else: diff --git a/resources/updates.py b/resources/updates.py index d5a62dcab..863f3a0f6 100644 --- a/resources/updates.py +++ b/resources/updates.py @@ -35,6 +35,12 @@ class CheckBinaryUpdates: if local_version is None: local_version = self.binary_version_array + + if local_version == remote_version: + if not self.constants.commit_info[0].startswith("refs/tags"): + # Check for nightly builds + return True + # Pad version numbers to match length (ie. 0.1.0 vs 0.1.0.1) while len(remote_version) > len(local_version): local_version.append(0) @@ -99,6 +105,9 @@ class CheckBinaryUpdates: response = network_handler.NetworkUtilities().get(REPO_LATEST_RELEASE_URL) data_set = response.json() + if "tag_name" not in data_set: + return None + self.remote_version = data_set["tag_name"] self.remote_version_array = self.remote_version.split(".") diff --git a/resources/wx_gui/gui_download.py b/resources/wx_gui/gui_download.py index de6864edf..c957ea1ae 100644 --- a/resources/wx_gui/gui_download.py +++ b/resources/wx_gui/gui_download.py @@ -8,7 +8,7 @@ class DownloadFrame(wx.Frame): """ Update provided frame with download stats """ - def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, download_obj: network_handler.DownloadObject, item_name: str, screen_location: tuple = None): + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, download_obj: network_handler.DownloadObject, item_name: str): self.constants: constants.Constants = global_constants self.title: str = title diff --git a/resources/wx_gui/gui_entry.py b/resources/wx_gui/gui_entry.py index 9d3269320..575876738 100644 --- a/resources/wx_gui/gui_entry.py +++ b/resources/wx_gui/gui_entry.py @@ -9,7 +9,8 @@ from resources.wx_gui import ( gui_build, gui_install_oc, gui_sys_patch, - gui_support + gui_support, + gui_update, ) from resources.sys_patch import sys_patch_detect @@ -21,6 +22,7 @@ class SupportedEntryPoints: BUILD_OC = gui_build.BuildFrame INSTALL_OC = gui_install_oc.InstallOCFrame SYS_PATCH = gui_sys_patch.SysPatchMenu + UPDATE_APP = gui_update.UpdateFrame class EntryPoint: diff --git a/resources/wx_gui/gui_macos_installer_download.py b/resources/wx_gui/gui_macos_installer_download.py index 31eeac4d0..f9627f780 100644 --- a/resources/wx_gui/gui_macos_installer_download.py +++ b/resources/wx_gui/gui_macos_installer_download.py @@ -170,7 +170,6 @@ class macOSInstallerFrame(wx.Frame): self, title=self.title, global_constants=self.constants, - screen_location=self.GetScreenPosition(), download_obj=download_obj, item_name=f"macOS {app['Version']} ({app['Build']})", ) diff --git a/resources/wx_gui/gui_main_menu.py b/resources/wx_gui/gui_main_menu.py index 9b712fbdc..3177092b9 100644 --- a/resources/wx_gui/gui_main_menu.py +++ b/resources/wx_gui/gui_main_menu.py @@ -1,4 +1,9 @@ import wx +import logging +import webbrowser +import threading +import sys + from resources.wx_gui import ( gui_build, gui_macos_installer_download, @@ -6,8 +11,10 @@ from resources.wx_gui import ( gui_support, gui_help, gui_settings, + gui_update, ) -from resources import constants + +from resources import constants, global_settings, updates from data import os_data class MainMenu(wx.Frame): @@ -25,6 +32,8 @@ class MainMenu(wx.Frame): self.SetPosition(screen_location) if screen_location else self.Centre() self.Show() + self._preflight_checks() + def _generate_elements(self) -> None: """ @@ -85,6 +94,61 @@ class MainMenu(wx.Frame): self.SetSize((350, copy_label.GetPosition()[1] + 50)) + def _preflight_checks(self): + if ( + self.constants.computer.build_model != None and + self.constants.computer.build_model != self.constants.computer.real_model and + self.constants.host_is_hackintosh is False + ): + # Notify user they're booting an unsupported configuration + pop_up = wx.MessageDialog( + self, + f"We found you are currently booting OpenCore built for a different unit: {self.constants.computer.build_model}\n\nWe builds configs to match individual units and cannot be mixed or reused with different Macs.\n\nPlease Build and Install a new OpenCore config, and reboot your Mac.", + "Unsupported Configuration Detected!", + style = wx.OK | wx.ICON_EXCLAMATION + ) + pop_up.ShowModal() + self.on_build_and_install() + return + + threading.Thread(target=self._check_for_updates).start() + + + def _check_for_updates(self): + if self.constants.has_checked_updates is True: + return + + ignore_updates = global_settings.GlobalEnviromentSettings().read_property("IgnoreAppUpdates") + if ignore_updates is True: + self.constants.ignore_updates = True + return + + self.constants.ignore_updates = False + self.constants.has_checked_updates = True + dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates() + if not dict: + return + + for entry in dict: + version = dict[entry]["Version"] + logging.info(f"New version: {version}") + dialog = wx.MessageDialog( + parent=self, + message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?", + caption="Update Available for OpenCore Legacy Patcher!", + style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION + ) + dialog.SetYesNoCancelLabels("Download and install", "Always Ignore", "Ignore Once") + response = dialog.ShowModal() + + if response == wx.ID_YES: + wx.CallAfter(self.on_update, dict[entry]["Link"], version) + elif response == wx.ID_NO: + logging.info("- Setting IgnoreAppUpdates to True") + self.constants.ignore_updates = True + global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", True) + + def on_build_and_install(self, event: wx.Event = None): self.Hide() gui_build.BuildFrame( @@ -131,3 +195,13 @@ class MainMenu(wx.Frame): global_constants=self.constants, screen_location=self.GetPosition() ) + + def on_update(self, oclp_url: str, oclp_version: str): + gui_update.UpdateFrame( + parent=self, + title=self.title, + global_constants=self.constants, + screen_location=self.GetPosition(), + url=oclp_url, + item=oclp_version + ) \ No newline at end of file diff --git a/resources/wx_gui/gui_sys_patch.py b/resources/wx_gui/gui_sys_patch.py index 57d187b9d..fa1414897 100644 --- a/resources/wx_gui/gui_sys_patch.py +++ b/resources/wx_gui/gui_sys_patch.py @@ -95,7 +95,6 @@ class SysPatchMenu(wx.Frame): self, title=self.title, global_constants=self.constants, - screen_location=self.GetScreenPosition(), download_obj=kdk_download_obj, item_name=f"KDK Build {self.kdk_obj.kdk_url_build}" ) diff --git a/resources/wx_gui/gui_update.py b/resources/wx_gui/gui_update.py new file mode 100644 index 000000000..1ee188b8b --- /dev/null +++ b/resources/wx_gui/gui_update.py @@ -0,0 +1,205 @@ +import wx +import sys +import subprocess +import threading +import logging +import time +from pathlib import Path + +from resources.wx_gui import gui_download + + +from resources import constants, network_handler, updates + +class UpdateFrame(wx.Frame): + + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: wx.Point, url: str = "", item: str = "") -> None: + if parent: + self.parent: wx.Frame = parent + + for child in self.parent.GetChildren(): + child.Hide() + parent.Hide() + else: + super(UpdateFrame, self).__init__(parent, title=title, size=(350, 300), style = wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) + + self.title: str = title + self.constants: constants.Constants = global_constants + self.application_path = self.constants.payload_path / "OpenCore-Patcher.app" + self.screen_location: wx.Point = screen_location + if self.screen_location is None: + if parent: + self.screen_location = parent.GetScreenPosition() + else: + self.Centre() + self.screen_location = self.GetScreenPosition() + + + if url == "" or item == "": + dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates() + if dict: + for key in dict: + item = dict[key]["Version"] + url = dict[key]["Link"] + break + + self.frame: wx.Frame = wx.Frame( + parent=parent if parent else self, + title=self.title, + size=(350, 130), + pos=self.screen_location, + style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX + ) + + + # Title: Preparing update + title_label = wx.StaticText(self.frame, label="Preparing download...", pos=(-1,1)) + title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + title_label.Center(wx.HORIZONTAL) + + # Progress bar + progress_bar = wx.Gauge(self.frame, range=100, pos=(10, 50), size=(300, 20)) + progress_bar.Center(wx.HORIZONTAL) + progress_bar.Pulse() + self.progress_bar = progress_bar + + self.frame.Show() + wx.Yield() + + download_obj = network_handler.DownloadObject(url, self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip") + + gui_download.DownloadFrame( + self.frame, + title=self.title, + global_constants=self.constants, + download_obj=download_obj, + item_name=f"OpenCore Patcher {item}" + ) + + if download_obj.download_complete is False: + progress_bar.SetValue(0) + wx.MessageBox("Failed to download update. If you continue to have this issue, please manually download OpenCore Legacy Patcher off Github", "Critical Error!", wx.OK | wx.ICON_ERROR) + sys.exit(1) + + # Title: Extracting update + title_label.SetLabel("Extracting update...") + title_label.Center(wx.HORIZONTAL) + wx.Yield() + + thread = threading.Thread(target=self._extract_update) + thread.start() + + while thread.is_alive(): + wx.Yield() + + # Title: Installing update + title_label.SetLabel("Installing update...") + title_label.Center(wx.HORIZONTAL) + + thread = threading.Thread(target=self._install_update) + thread.start() + + while thread.is_alive(): + wx.Yield() + + # Title: Update complete + title_label.SetLabel("Update complete!") + title_label.Center(wx.HORIZONTAL) + + # Progress bar + progress_bar.Hide() + + # Label: 0.6.6 has been installed to: + installed_label = wx.StaticText(self.frame, label=f"{item} has been installed:", pos=(-1, progress_bar.GetPosition().y - 15)) + installed_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + installed_label.Center(wx.HORIZONTAL) + + # Label: '/Library/Application Support/Dortania' + installed_path_label = wx.StaticText(self.frame, label='/Library/Application Support/Dortania', pos=(-1, installed_label.GetPosition().y + 20)) + installed_path_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + installed_path_label.Center(wx.HORIZONTAL) + + # Label: Launching update shortly... + launch_label = wx.StaticText(self.frame, label="Launching update shortly...", pos=(-1, installed_path_label.GetPosition().y + 20)) + launch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + launch_label.Center(wx.HORIZONTAL) + + # Adjust frame size + self.frame.SetSize((-1, launch_label.GetPosition().y + 80)) + + thread = threading.Thread(target=self._launch_update) + thread.start() + + while thread.is_alive(): + wx.Yield() + + timer = 3 + while True: + wx.GetApp().Yield() + launch_label.SetLabel(f"Closing old process in {timer} seconds") + launch_label.Center(wx.HORIZONTAL) + time.sleep(1) + timer -= 1 + if timer == 0: + break + + sys.exit(0) + + + + def _extract_update(self): + # Extract update + logging.info("Extracting update") + if Path(self.application_path).exists(): + subprocess.run(["rm", "-rf", str(self.application_path)]) + result = subprocess.run( + ["ditto", "-xk", str(self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip"), str(self.constants.payload_path)], capture_output=True + ) + if result.returncode != 0: + wx.CallAfter(self.progress_bar.SetValue, 0) + wx.CallAfter(wx.MessageBox, f"Failed to extract update. Error: {result.stderr.decode('utf-8')}", "Critical Error!", wx.OK | wx.ICON_ERROR) + wx.CallAfter(sys.exit, 1) + + + def _install_update(self): + # Install update + logging.info("Installing update") + + # Create bash script to run as root + script = f"""#!/bin/bash +# Check if '/Library/Application Support/Dortania' exists +if [ ! -d "/Library/Application Support/Dortania" ]; then + mkdir -p "/Library/Application Support/Dortania" +fi + +# Check if '/Library/Application Support/Dortania/OpenCore-Patcher.app' exists +if [ -d "/Library/Application Support/Dortania/OpenCore-Patcher.app" ]; then + rm -rf "/Library/Application Support/Dortania/OpenCore-Patcher.app" +fi + +# Move '/tmp/OpenCore-Patcher.app' to '/Library/Application Support/Dortania' +mv "{str(self.application_path)}" "/Library/Application Support/Dortania/OpenCore-Patcher.app" + +# Check if '/Applications/OpenCore-Patcher.app' exists +if [ -d "/Applications/OpenCore-Patcher.app" ]; then + ln -s "/Library/Application Support/Dortania/OpenCore-Patcher.app" "/Applications/OpenCore-Patcher.app" +fi +""" + # Write script to file + with open(self.constants.payload_path / "update.sh", "w") as f: + f.write(script) + + # Execute script + args = [self.constants.oclp_helper_path, "/bin/sh", str(self.constants.payload_path / "update.sh")] + logging.info(f"Executing: {args}") + result = subprocess.run(args, capture_output=True) + if result.returncode != 0: + wx.CallAfter(self.progress_bar.SetValue, 0) + wx.CallAfter(wx.MessageBox, f"Failed to install update. Error: {result.stderr.decode('utf-8')}", "Critical Error!", wx.OK | wx.ICON_ERROR) + wx.CallAfter(sys.exit, 1) + + + def _launch_update(self): + # Launch update + logging.info("Launching update: '/Library/Application Support/Dortania/OpenCore-Patcher.app'") + subprocess.Popen(["open", "/Library/Application Support/Dortania/OpenCore-Patcher.app"])