diff --git a/Build-Binary.command b/Build-Binary.command index 9251b27d2..e1c886f6b 100755 --- a/Build-Binary.command +++ b/Build-Binary.command @@ -30,7 +30,7 @@ class CreateBinary: def __init__(self): start = time.time() - print("- Starting build script") + print("Starting build script") self.args = self._parse_arguments() @@ -39,7 +39,7 @@ class CreateBinary: self._preflight_processes() self._build_binary() self._postflight_processes() - print(f"- Build script completed in {str(round(time.time() - start, 2))} seconds") + print(f"Build script completed in {str(round(time.time() - start, 2))} seconds") def _set_cwd(self): @@ -48,7 +48,7 @@ class CreateBinary: """ os.chdir(Path(__file__).resolve().parent) - print(f"- Current Working Directory: \n\t{os.getcwd()}") + print(f"Current Working Directory: \n\t{os.getcwd()}") def _parse_arguments(self): @@ -88,7 +88,7 @@ class CreateBinary: pyinstaller_path = f"{python_bin_dir}pyinstaller" if not Path(pyinstaller_path).exists(): - print(f" - pyinstaller not found:\n\t{pyinstaller_path}") + print(f"- pyinstaller not found:\n\t{pyinstaller_path}") raise Exception("pyinstaller not found") self.pyinstaller_path = pyinstaller_path @@ -99,7 +99,7 @@ class CreateBinary: Start preflight processes """ - print("- Starting preflight processes") + print("Starting preflight processes") self._setup_pathing() self._delete_extra_binaries() self._download_resources() @@ -111,7 +111,7 @@ class CreateBinary: Start postflight processes """ - print("- Starting postflight processes") + print("Starting postflight processes") self._patch_load_command() self._add_commit_data() self._post_flight_cleanup() @@ -124,19 +124,19 @@ class CreateBinary: """ if Path(f"./dist/OpenCore-Patcher.app").exists(): - print("- Found OpenCore-Patcher.app, removing...") + print("Found OpenCore-Patcher.app, removing...") rm_output = subprocess.run( ["rm", "-rf", "./dist/OpenCore-Patcher.app"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if rm_output.returncode != 0: - print("- Remove failed") + print("Remove failed") print(rm_output.stderr.decode('utf-8')) raise Exception("Remove failed") self._embed_key() - print("- Building GUI binary...") + print("Building GUI binary...") build_args = [self.pyinstaller_path, "./OpenCore-Patcher-GUI.spec", "--noconfirm"] build_result = subprocess.run(build_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -144,12 +144,12 @@ class CreateBinary: self._strip_key() if build_result.returncode != 0: - print("- Build failed") + print("Build failed") print(build_result.stderr.decode('utf-8')) raise Exception("Build failed") # Next embed support icns into ./Resources - print("- Embedding icns...") + print("Embedding icns...") for file in Path("payloads/Icon/AppIcons").glob("*.icns"): subprocess.run( ["cp", str(file), "./dist/OpenCore-Patcher.app/Contents/Resources/"], @@ -165,15 +165,15 @@ class CreateBinary: """ if not self.args.key: - print("- No developer key provided, skipping...") + print("No developer key provided, skipping...") return if not self.args.site: - print("- No site provided, skipping...") + print("No site provided, skipping...") return - print("- Embedding developer key...") + print("Embedding developer key...") if not Path("./resources/analytics_handler.py").exists(): - print("- analytics_handler.py not found") + print("analytics_handler.py not found") return lines = [] @@ -196,15 +196,15 @@ class CreateBinary: """ if not self.args.key: - print("- No developer key provided, skipping...") + print("No developer key provided, skipping...") return if not self.args.site: - print("- No site provided, skipping...") + print("No site provided, skipping...") return - print("- Stripping developer key...") + print("Stripping developer key...") if not Path("./resources/analytics_handler.py").exists(): - print("- analytics_handler.py not found") + print("analytics_handler.py not found") return lines = [] @@ -247,17 +247,17 @@ class CreateBinary: ] - print("- Deleting extra binaries...") + print("Deleting extra binaries...") for file in Path("payloads").glob(pattern="*"): if file.is_dir(): if file.name in whitelist_folders: continue - print(f" - Deleting {file.name}") + print(f"- Deleting {file.name}") subprocess.run(["rm", "-rf", file]) else: if file.name in whitelist_files: continue - print(f" - Deleting {file.name}") + print(f"- Deleting {file.name}") subprocess.run(["rm", "-f", file]) @@ -271,23 +271,23 @@ class CreateBinary: "Universal-Binaries.dmg" ] - print("- Downloading required resources...") + print("Downloading required resources...") for resource in required_resources: if Path(f"./{resource}").exists(): if self.args.reset_binaries: - print(f" - Removing old {resource}") + print(f"- Removing old {resource}") rm_output = subprocess.run( ["rm", "-rf", f"./{resource}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if rm_output.returncode != 0: - print("- Remove failed") + print("Remove failed") print(rm_output.stderr.decode('utf-8')) raise Exception("Remove failed") else: - print(f" - {resource} already exists, skipping download") + print(f"- {resource} already exists, skipping download") continue - print(f" - Downloading {resource}...") + print(f"- Downloading {resource}...") download_result = subprocess.run( [ @@ -298,11 +298,11 @@ class CreateBinary: ) if download_result.returncode != 0: - print(" - Download failed") + print("- Download failed") print(download_result.stderr.decode('utf-8')) raise Exception("Download failed") if not Path(f"./{resource}").exists(): - print(f" - {resource} not found") + print(f"- {resource} not found") raise Exception(f"{resource} not found") @@ -315,20 +315,20 @@ class CreateBinary: if Path("./payloads.dmg").exists(): if not self.args.reset_binaries: - print(" - payloads.dmg already exists, skipping creation") + print("- payloads.dmg already exists, skipping creation") return - print(" - Removing old payloads.dmg") + print("- Removing old payloads.dmg") rm_output = subprocess.run( ["rm", "-rf", "./payloads.dmg"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if rm_output.returncode != 0: - print("- Remove failed") + print("Remove failed") print(rm_output.stderr.decode('utf-8')) raise Exception("Remove failed") - print(" - Generating DMG...") + print("- Generating DMG...") dmg_output = subprocess.run([ 'hdiutil', 'create', './payloads.dmg', '-megabytes', '32000', # Overlays can only be as large as the disk image allows @@ -339,11 +339,11 @@ class CreateBinary: '-passphrase', 'password', '-encryption' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if dmg_output.returncode != 0: - print(" - DMG generation failed") + print("- DMG generation failed") print(dmg_output.stderr.decode('utf-8')) raise Exception("DMG generation failed") - print(" - DMG generation complete") + print("- DMG generation complete") def _add_commit_data(self): @@ -352,7 +352,7 @@ class CreateBinary: """ if not self.args.branch and not self.args.commit and not self.args.commit_date: - print(" - No commit data provided, adding source info") + print("- No commit data provided, adding source info") branch = "Built from source" commit_url = "" commit_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) @@ -360,7 +360,7 @@ class CreateBinary: branch = self.args.branch commit_url = self.args.commit commit_date = self.args.commit_date - print(" - Adding commit data to Info.plist") + print("- Adding commit data to Info.plist") plist_path = Path("./dist/OpenCore-Patcher.app/Contents/Info.plist") plist = plistlib.load(Path(plist_path).open("rb")) plist["Github"] = { @@ -389,7 +389,7 @@ class CreateBinary: """ - print(" - Patching LC_VERSION_MIN_MACOSX") + print("- Patching LC_VERSION_MIN_MACOSX") path = './dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher' find = b'\x00\x0D\x0A\x00' # 10.13 (0xA0D) replace = b'\x00\x0A\x0A\x00' # 10.10 (0xA0A) @@ -406,13 +406,13 @@ class CreateBinary: """ path = "./dist/OpenCore-Patcher" - print(f" - Removing {path}") + print(f"- Removing {path}") rm_output = subprocess.run( ["rm", "-rf", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if rm_output.returncode != 0: - print(f" - Remove failed: {path}") + print(f"- Remove failed: {path}") print(rm_output.stderr.decode('utf-8')) raise Exception(f"Remove failed: {path}") @@ -422,13 +422,13 @@ class CreateBinary: Validate generated binary """ - print(" - Validating binary") + print("- Validating binary") validate_output = subprocess.run( ["./dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher", "--build", "--model", "MacPro3,1"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) if validate_output.returncode != 0: - print(" - Validation failed") + print("- Validation failed") print(validate_output.stderr.decode('utf-8')) raise Exception("Validation failed") diff --git a/CHANGELOG.md b/CHANGELOG.md index abf13d87d..35a6dd0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,33 @@ - Allows for Live Text support on systems with3802 GPUs - ie. Intel Ivy Bridge and Haswell, Nvidia Kepler - Previously disabled due to high instability in Photos with Face Scanning, now resolved -- Resolve crashing after patching with MenuBar2 implementation enabled +- Work-around crashing after patching with MenuBar2 implementation enabled - Setting must be re-enabled after patching +- Update non-Metal Binaries: + - Resolve window placement defaulting past top of screen for some apps + - ex. OpenCore-Patcher.app during root patching + - Resolve indeterminate progress bars not rendering with wxWidgets in Monterey and later + - ex. OpenCore-Patcher.app +- UI changes: + - Add "Show Log File" button to menubar - Backend changes: - Call `setpgrp()` to prevent app from being killed if parent process is killed (ie. LaunchAgents) + - Rework logging handler: + - Implement formatted logging + - Allowing easier debugging + - Implement per-version, per-run file logging + - ex. OpenCore-Patcher (0.6.7) (2021-12-31-12-34-56).log + - Keep only 10 latest log files + - Reveal log file in Finder on main thread crash + - Avoid writing username to log file + - Resolve SharedSupport.dmg pathing error during macOS Installer Verification + - Applicable to systems with 2 (or more) USB Installers with the same name plugged in - Resolve payloads path being mis-routed during CLI calls + - Add UI when fetching root patches for host + - Remove progress bar work-around for non-Metal in Monterey and later + - Requires host to have been patched with PatcherSupportPkg 1.1.2 or newer +- Increment Binaries: + - PatcherSupportPkg 1.1.2 - release ## 0.6.6 - Implement option to disable ColorSync downgrade on HD 3000 Macs diff --git a/resources/analytics_handler.py b/resources/analytics_handler.py index 64e276966..637bed005 100644 --- a/resources/analytics_handler.py +++ b/resources/analytics_handler.py @@ -9,8 +9,9 @@ from resources import network_handler, constants, global_settings DATE_FORMAT: str = "%Y-%m-%d %H-%M-%S" ANALYTICS_SERVER: str = "" SITE_KEY: str = "" +CRASH_URL: str = ANALYTICS_SERVER + "/crash" -VALID_ENTRIES: dict = { +VALID_ANALYTICS_ENTRIES: dict = { 'KEY': str, # Prevent abuse (embedded at compile time) 'UNIQUE_IDENTITY': str, # Host's UUID as SHA1 hash 'APPLICATION_NAME': str, # ex. OpenCore Legacy Patcher @@ -23,17 +24,60 @@ VALID_ENTRIES: dict = { 'TIMESTAMP': datetime.datetime, # ex. 2021-09-01-12-00-00 } +VALID_CRASH_ENTRIES: dict = { + 'KEY': str, # Prevent abuse (embedded at compile time) + 'APPLICATION_VERSION': str, # ex. 0.2.0 + 'APPLICATION_COMMIT': str, # ex. 0.2.0 or {commit hash if not a release} + 'OS_VERSION': str, # ex. 10.15.7 + 'MODEL': str, # ex. MacBookPro11,5 + 'TIMESTAMP': datetime.datetime, # ex. 2021-09-01-12-00-00 + 'CRASH_LOG': str, # ex. "This is a crash log" +} + class Analytics: def __init__(self, global_constants: constants.Constants) -> None: self.constants: constants.Constants = global_constants + self.unique_identity = str(self.constants.computer.uuid_sha1) + self.application = str("OpenCore Legacy Patcher") + self.version = str(self.constants.patcher_version) + self.os = str(self.constants.detected_os_version) + self.model = str(self.constants.computer.real_model) + self.date = str(datetime.datetime.now().strftime(DATE_FORMAT)) + + def send_analytics(self) -> None: if global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting") is True: return self._generate_base_data() - self._post_data() + self._post_analytics_data() + + + def send_crash_report(self, log_file: Path) -> None: + if ANALYTICS_SERVER == "": + return + if SITE_KEY == "": + return + if global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting") is True: + return + if not log_file.exists(): + return + + commit_info = self.constants.commit_info[0].split("/")[-1] + "_" + self.constants.commit_info[1].split("T")[0] + "_" + self.constants.commit_info[2].split("/")[-1] + + crash_data= { + "KEY": SITE_KEY, + "APPLICATION_VERSION": self.version, + "APPLICATION_COMMIT": commit_info, + "OS_VERSION": self.os, + "MODEL": self.model, + "TIMESTAMP": self.date, + "CRASH_LOG": log_file.read_text() + } + + network_handler.NetworkUtilities().post(CRASH_URL, json = crash_data) def _get_country(self) -> str: @@ -54,12 +98,6 @@ class Analytics: def _generate_base_data(self) -> None: - - self.unique_identity = str(self.constants.computer.uuid_sha1) - self.application = str("OpenCore Legacy Patcher") - self.version = str(self.constants.patcher_version) - self.os = str( self.constants.detected_os_version) - self.model = str(self.constants.computer.real_model) self.gpus = [] self.firmware = str(self.constants.computer.firmware_vendor) @@ -78,14 +116,14 @@ class Analytics: 'GPUS': self.gpus, 'FIRMWARE': self.firmware, 'LOCATION': self.location, - 'TIMESTAMP': str(datetime.datetime.now().strftime(DATE_FORMAT)), + 'TIMESTAMP': self.date, } # convert to JSON: self.data = json.dumps(self.data) - def _post_data(self) -> None: + def _post_analytics_data(self) -> None: # Post data to analytics server if ANALYTICS_SERVER == "": return diff --git a/resources/arguments.py b/resources/arguments.py index c9568283f..62d6eb3d7 100644 --- a/resources/arguments.py +++ b/resources/arguments.py @@ -50,7 +50,7 @@ class arguments: """ Enter validation mode """ - + logging.info("Set Validation Mode") validation.PatcherValidation(self.constants) @@ -59,9 +59,9 @@ class arguments: Start root volume patching """ - logging.info("- Set System Volume patching") + logging.info("Set System Volume patching") if "Library/InstallerSandboxes/" in str(self.constants.payload_path): - logging.info("- Running from Installer Sandbox") + logging.info("- Running from Installer Sandbox, blocking OS updaters") thread = threading.Thread(target=sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_patch) thread.start() while thread.is_alive(): @@ -75,7 +75,7 @@ class arguments: """ Start root volume unpatching """ - logging.info("- Set System Volume unpatching") + logging.info("Set System Volume unpatching") sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_unpatch() @@ -84,7 +84,7 @@ class arguments: Start root volume auto patching """ - logging.info("- Set Auto patching") + logging.info("Set Auto patching") sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch() @@ -92,6 +92,7 @@ class arguments: """ Start config building process """ + logging.info("Set OpenCore Build") if self.args.model: if self.args.model: diff --git a/resources/build/support.py b/resources/build/support.py index a79b14140..5f0bcf43c 100644 --- a/resources/build/support.py +++ b/resources/build/support.py @@ -136,7 +136,7 @@ class BuildSupport: for acpi in config_plist["ACPI"]["Add"]: if not Path(self.constants.opencore_release_folder / Path("EFI/OC/ACPI") / Path(acpi["Path"])).exists(): - logging.info(f" - Missing ACPI Table: {acpi['Path']}") + logging.info(f"- Missing ACPI Table: {acpi['Path']}") raise Exception(f"Missing ACPI Table: {acpi['Path']}") for kext in config_plist["Kernel"]["Add"]: @@ -155,19 +155,19 @@ class BuildSupport: for tool in config_plist["Misc"]["Tools"]: if not Path(self.constants.opencore_release_folder / Path("EFI/OC/Tools") / Path(tool["Path"])).exists(): - logging.info(f" - Missing tool: {tool['Path']}") + logging.info(f"- Missing tool: {tool['Path']}") raise Exception(f"Missing tool: {tool['Path']}") for driver in config_plist["UEFI"]["Drivers"]: if not Path(self.constants.opencore_release_folder / Path("EFI/OC/Drivers") / Path(driver["Path"])).exists(): - logging.info(f" - Missing driver: {driver['Path']}") + logging.info(f"- Missing driver: {driver['Path']}") raise Exception(f"Missing driver: {driver['Path']}") # Validating local files # Report if they have no associated config.plist entry (i.e. they're not being used) for tool_files in Path(self.constants.opencore_release_folder / Path("EFI/OC/Tools")).glob("*"): if tool_files.name not in [x["Path"] for x in config_plist["Misc"]["Tools"]]: - logging.info(f" - Missing tool from config: {tool_files.name}") + logging.info(f"- Missing tool from config: {tool_files.name}") raise Exception(f"Missing tool from config: {tool_files.name}") for driver_file in Path(self.constants.opencore_release_folder / Path("EFI/OC/Drivers")).glob("*"): diff --git a/resources/constants.py b/resources/constants.py index 87d6577b2..70d7430fa 100644 --- a/resources/constants.py +++ b/resources/constants.py @@ -13,7 +13,7 @@ class Constants: def __init__(self) -> None: # Patcher Versioning self.patcher_version: str = "0.6.7" # OpenCore-Legacy-Patcher - self.patcher_support_pkg_version: str = "1.1.0" # PatcherSupportPkg + self.patcher_support_pkg_version: str = "1.1.2" # PatcherSupportPkg self.copyright_date: str = "Copyright © 2020-2023 Dortania" self.patcher_name: str = "OpenCore Legacy Patcher" @@ -134,6 +134,7 @@ class Constants: self.booted_oc_disk: str = None # Determine current disk OCLP booted from self.unpack_thread = None # Determine if unpack thread finished (threading.Thread) self.update_stage: int = 0 # Determine update stage (see gui_support.py) + self.log_filepath: Path = None # Path to log file self.commit_info: tuple = (None, None, None) # Commit info (Branch, Commit Date, Commit URL) diff --git a/resources/global_settings.py b/resources/global_settings.py index de8b3db80..0cb4645a1 100644 --- a/resources/global_settings.py +++ b/resources/global_settings.py @@ -48,7 +48,7 @@ class GlobalEnviromentSettings: try: plistlib.dump(plist, Path(self.global_settings_plist).open("wb")) except PermissionError: - logging.info("- Failed to write to global settings file") + logging.info("Failed to write to global settings file") def _generate_settings_file(self) -> None: @@ -57,7 +57,7 @@ class GlobalEnviromentSettings: try: plistlib.dump({"Developed by Dortania": True,}, Path(self.global_settings_plist).open("wb")) except PermissionError: - logging.info("- Permission error: Unable to write to global settings file") + logging.info("Permission error: Unable to write to global settings file") def _convert_defaults_to_global_settings(self) -> None: @@ -76,14 +76,14 @@ class GlobalEnviromentSettings: try: plistlib.dump(global_settings_plist, Path(self.global_settings_plist).open("wb")) except PermissionError: - logging.info("- Permission error: Unable to write to global settings file") + logging.info("Permission error: Unable to write to global settings file") return # delete defaults plist try: Path(defaults_path).unlink() except PermissionError: - logging.info("- Permission error: Unable to delete defaults plist") + logging.info("Permission error: Unable to delete defaults plist") def _fix_file_permission(self) -> None: @@ -100,6 +100,6 @@ class GlobalEnviromentSettings: # Set file permission to allow any user to write to log file result = subprocess.run(["chmod", "777", self.global_settings_plist], capture_output=True) if result.returncode != 0: - logging.warning("- Failed to fix settings file permissions:") + logging.warning("Failed to fix settings file permissions:") if result.stderr: logging.warning(result.stderr.decode("utf-8")) \ No newline at end of file diff --git a/resources/install.py b/resources/install.py index 1ff497de5..6fe7e11ef 100644 --- a/resources/install.py +++ b/resources/install.py @@ -88,13 +88,13 @@ class tui_disk_installation: def install_opencore(self, full_disk_identifier: str): # TODO: Apple Script fails in Yosemite(?) and older - logging.info(f"- Mounting partition: {full_disk_identifier}") + logging.info(f"Mounting partition: {full_disk_identifier}") if self.constants.detected_os >= os_data.os_data.el_capitan and not self.constants.recovery_status: try: applescript.AppleScript(f'''do shell script "diskutil mount {full_disk_identifier}" with prompt "OpenCore Legacy Patcher needs administrator privileges to mount this volume." with administrator privileges without altering line endings''').run() except applescript.ScriptError as e: if "User canceled" in str(e): - logging.info("- Mount cancelled by user") + logging.info("Mount cancelled by user") return logging.info(f"An error occurred: {e}") if utilities.check_boot_mode() == "safe_boot": @@ -104,7 +104,7 @@ class tui_disk_installation: else: result = subprocess.run(f"diskutil mount {full_disk_identifier}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: - logging.info("- Mount failed") + logging.info("Mount failed") logging.info(result.stderr.decode()) return @@ -124,18 +124,18 @@ class tui_disk_installation: return False if (mount_path / Path("EFI/OC")).exists(): - logging.info("- Removing preexisting EFI/OC folder") + logging.info("Removing preexisting EFI/OC folder") subprocess.run(["rm", "-rf", mount_path / Path("EFI/OC")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if (mount_path / Path("System")).exists(): - logging.info("- Removing preexisting System folder") + logging.info("Removing preexisting System folder") subprocess.run(["rm", "-rf", mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if (mount_path / Path("boot.efi")).exists(): - logging.info("- Removing preexisting boot.efi") + logging.info("Removing preexisting boot.efi") subprocess.run(["rm", mount_path / Path("boot.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - logging.info("- Copying OpenCore onto EFI partition") + logging.info("Copying OpenCore onto EFI partition") subprocess.run(["mkdir", "-p", mount_path / Path("EFI")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(["cp", "-r", self.constants.opencore_release_folder / Path("EFI/OC"), mount_path / Path("EFI/OC")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.run(["cp", "-r", self.constants.opencore_release_folder / Path("System"), mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -144,7 +144,7 @@ class tui_disk_installation: subprocess.run(["cp", self.constants.opencore_release_folder / Path("boot.efi"), mount_path / Path("boot.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if self.constants.boot_efi is True: - logging.info("- Converting Bootstrap to BOOTx64.efi") + logging.info("Converting Bootstrap to BOOTx64.efi") if (mount_path / Path("EFI/BOOT")).exists(): subprocess.run(["rm", "-rf", mount_path / Path("EFI/BOOT")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) Path(mount_path / Path("EFI/BOOT")).mkdir() @@ -152,23 +152,23 @@ class tui_disk_installation: subprocess.run(["rm", "-rf", mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if self._determine_sd_card(sd_type) is True: - logging.info("- Adding SD Card icon") + logging.info("Adding SD Card icon") subprocess.run(["cp", self.constants.icon_path_sd, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) elif ssd_type is True: - logging.info("- Adding SSD icon") + logging.info("Adding SSD icon") subprocess.run(["cp", self.constants.icon_path_ssd, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) elif disk_type == "USB": - logging.info("- Adding External USB Drive icon") + logging.info("Adding External USB Drive icon") subprocess.run(["cp", self.constants.icon_path_external, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) else: - logging.info("- Adding Internal Drive icon") + logging.info("Adding Internal Drive icon") subprocess.run(["cp", self.constants.icon_path_internal, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - logging.info("- Cleaning install location") + logging.info("Cleaning install location") if not self.constants.recovery_status: - logging.info("- Unmounting EFI partition") + logging.info("Unmounting EFI partition") subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode() - logging.info("- OpenCore transfer complete") + logging.info("OpenCore transfer complete") return True \ No newline at end of file diff --git a/resources/kdk_handler.py b/resources/kdk_handler.py index be91bb8ca..0c59bf13d 100644 --- a/resources/kdk_handler.py +++ b/resources/kdk_handler.py @@ -94,7 +94,7 @@ class KernelDebugKitObject: global KDK_ASSET_LIST - logging.info("- Pulling KDK list from KdkSupportPkg API") + logging.info("Pulling KDK list from KdkSupportPkg API") if KDK_ASSET_LIST: return KDK_ASSET_LIST @@ -107,11 +107,11 @@ class KernelDebugKitObject: timeout=5 ) except (requests.exceptions.Timeout, requests.exceptions.TooManyRedirects, requests.exceptions.ConnectionError): - logging.info("- Could not contact KDK API") + logging.info("Could not contact KDK API") return None if results.status_code != 200: - logging.info("- Could not fetch KDK list") + logging.info("Could not fetch KDK list") return None KDK_ASSET_LIST = sorted(results.json(), key=lambda x: (packaging.version.parse(x["version"]), datetime.datetime.fromisoformat(x["date"])), reverse=True) @@ -138,12 +138,12 @@ class KernelDebugKitObject: if os_data.os_conversion.os_to_kernel(str(parsed_version.major)) < os_data.os_data.ventura: self.error_msg = "KDKs are not required for macOS Monterey or older" - logging.warning(f"- {self.error_msg}") + logging.warning(f"{self.error_msg}") return self.kdk_installed_path = self._local_kdk_installed() if self.kdk_installed_path: - logging.info(f"- KDK already installed ({Path(self.kdk_installed_path).name}), skipping") + logging.info(f"KDK already installed ({Path(self.kdk_installed_path).name}), skipping") self.kdk_already_installed = True self.success = True return @@ -151,29 +151,29 @@ class KernelDebugKitObject: remote_kdk_version = self._get_remote_kdks() if remote_kdk_version is None: - logging.warning("- Failed to fetch KDK list, falling back to local KDK matching") + logging.warning("Failed to fetch KDK list, falling back to local KDK matching") # First check if a KDK matching the current macOS version is installed # ex. 13.0.1 vs 13.0 loose_version = f"{parsed_version.major}.{parsed_version.minor}" - logging.info(f"- Checking for KDKs loosely matching {loose_version}") + logging.info(f"Checking for KDKs loosely matching {loose_version}") self.kdk_installed_path = self._local_kdk_installed(match=loose_version, check_version=True) if self.kdk_installed_path: - logging.info(f"- Found matching KDK: {Path(self.kdk_installed_path).name}") + logging.info(f"Found matching KDK: {Path(self.kdk_installed_path).name}") self.kdk_already_installed = True self.success = True return older_version = f"{parsed_version.major}.{parsed_version.minor - 1 if parsed_version.minor > 0 else 0}" - logging.info(f"- Checking for KDKs matching {older_version}") + logging.info(f"Checking for KDKs matching {older_version}") self.kdk_installed_path = self._local_kdk_installed(match=older_version, check_version=True) if self.kdk_installed_path: - logging.info(f"- Found matching KDK: {Path(self.kdk_installed_path).name}") + logging.info(f"Found matching KDK: {Path(self.kdk_installed_path).name}") self.kdk_already_installed = True self.success = True return - logging.warning(f"- Couldn't find KDK matching {host_version} or {older_version}, please install one manually") + logging.warning(f"Couldn't find KDK matching {host_version} or {older_version}, please install one manually") self.error_msg = f"Could not contact KdkSupportPkg API, and no KDK matching {host_version} ({host_build}) or {older_version} was installed.\nPlease ensure you have a network connection or manually install a KDK." @@ -211,32 +211,32 @@ class KernelDebugKitObject: if self.kdk_url == "": if self.kdk_closest_match_url == "": - logging.warning(f"- No KDKs found for {host_build} ({host_version})") + logging.warning(f"No KDKs found for {host_build} ({host_version})") self.error_msg = f"No KDKs found for {host_build} ({host_version})" return - logging.info(f"- No direct match found for {host_build}, falling back to closest match") - logging.info(f"- Closest Match: {self.kdk_closest_match_url_build} ({self.kdk_closest_match_url_version})") + logging.info(f"No direct match found for {host_build}, falling back to closest match") + logging.info(f"Closest Match: {self.kdk_closest_match_url_build} ({self.kdk_closest_match_url_version})") self.kdk_url = self.kdk_closest_match_url self.kdk_url_build = self.kdk_closest_match_url_build self.kdk_url_version = self.kdk_closest_match_url_version self.kdk_url_expected_size = self.kdk_closest_match_url_expected_size else: - logging.info(f"- Direct match found for {host_build} ({host_version})") + logging.info(f"Direct match found for {host_build} ({host_version})") # Check if this KDK is already installed self.kdk_installed_path = self._local_kdk_installed(match=self.kdk_url_build) if self.kdk_installed_path: - logging.info(f"- KDK already installed ({Path(self.kdk_installed_path).name}), skipping") + logging.info(f"KDK already installed ({Path(self.kdk_installed_path).name}), skipping") self.kdk_already_installed = True self.success = True return - logging.info("- Following KDK is recommended:") - logging.info(f"- KDK Build: {self.kdk_url_build}") - logging.info(f"- KDK Version: {self.kdk_url_version}") - logging.info(f"- KDK URL: {self.kdk_url}") + logging.info("Following KDK is recommended:") + logging.info(f"- KDK Build: {self.kdk_url_build}") + logging.info(f"- KDK Version: {self.kdk_url_version}") + logging.info(f"- KDK URL: {self.kdk_url}") self.success = True @@ -256,7 +256,7 @@ class KernelDebugKitObject: self.error_msg = "" if self.kdk_already_installed: - logging.info("- No download required, KDK already installed") + logging.info("No download required, KDK already installed") self.success = True return None @@ -265,7 +265,7 @@ class KernelDebugKitObject: logging.error(self.error_msg) return None - logging.info(f"- Returning DownloadObject for KDK: {Path(self.kdk_url).name}") + logging.info(f"Returning DownloadObject for KDK: {Path(self.kdk_url).name}") self.success = True kdk_download_path = self.constants.kdk_download_path if override_path == "" else Path(override_path) @@ -294,7 +294,7 @@ class KernelDebugKitObject: plist_path.touch() plistlib.dump(kdk_dict, plist_path.open("wb"), sort_keys=False) except Exception as e: - logging.error(f"- Failed to generate KDK Info.plist: {e}") + logging.error(f"Failed to generate KDK Info.plist: {e}") def _local_kdk_valid(self, kdk_path: Path) -> bool: @@ -314,14 +314,14 @@ class KernelDebugKitObject: """ if not Path(f"{kdk_path}/System/Library/CoreServices/SystemVersion.plist").exists(): - logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing SystemVersion.plist") + logging.info(f"Corrupted KDK found ({kdk_path.name}), removing due to missing SystemVersion.plist") self._remove_kdk(kdk_path) return False # Get build from KDK kdk_plist_data = plistlib.load(Path(f"{kdk_path}/System/Library/CoreServices/SystemVersion.plist").open("rb")) if "ProductBuildVersion" not in kdk_plist_data: - logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing ProductBuildVersion") + logging.info(f"Corrupted KDK found ({kdk_path.name}), removing due to missing ProductBuildVersion") self._remove_kdk(kdk_path) return False @@ -331,7 +331,7 @@ class KernelDebugKitObject: result = subprocess.run(["pkgutil", "--files", f"com.apple.pkg.KDK.{kdk_build}"], capture_output=True) if result.returncode != 0: # If pkg receipt is missing, we'll fallback to legacy validation - logging.info(f"- pkg receipt missing for {kdk_path.name}, falling back to legacy validation") + logging.info(f"pkg receipt missing for {kdk_path.name}, falling back to legacy validation") return self._local_kdk_valid_legacy(kdk_path) # Go through each line of the pkg receipt and ensure it exists @@ -339,7 +339,7 @@ class KernelDebugKitObject: if not line.startswith("System/Library/Extensions"): continue if not Path(f"{kdk_path}/{line}").exists(): - logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing file: {line}") + logging.info(f"Corrupted KDK found ({kdk_path.name}), removing due to missing file: {line}") self._remove_kdk(kdk_path) return False @@ -368,7 +368,7 @@ class KernelDebugKitObject: for kext in KEXT_CATALOG: if not Path(f"{kdk_path}/System/Library/Extensions/{kext}").exists(): - logging.info(f"- Corrupted KDK found, removing due to missing: {kdk_path}/System/Library/Extensions/{kext}") + logging.info(f"Corrupted KDK found, removing due to missing: {kdk_path}/System/Library/Extensions/{kext}") self._remove_kdk(kdk_path) return False @@ -427,15 +427,15 @@ class KernelDebugKitObject: if not kdk_pkg.name.endswith(f"{match}.pkg"): continue - logging.info(f"- Found KDK backup: {kdk_pkg.name}") + logging.info(f"Found KDK backup: {kdk_pkg.name}") if self.passive is False: - logging.info("- Attempting KDK restoration") + logging.info("Attempting KDK restoration") if KernelDebugKitUtilities().install_kdk_pkg(kdk_pkg): - logging.info("- Successfully restored KDK") + logging.info("Successfully restored KDK") return self._local_kdk_installed(match=match, check_version=check_version) else: # When in passive mode, we're just checking if a KDK could be restored - logging.info("- KDK restoration skipped, running in passive mode") + logging.info("KDK restoration skipped, running in passive mode") return kdk_pkg return None @@ -453,22 +453,22 @@ class KernelDebugKitObject: return if os.getuid() != 0: - logging.warning("- Cannot remove KDK, not running as root") + logging.warning("Cannot remove KDK, not running as root") return if not Path(kdk_path).exists(): - logging.warning(f"- KDK does not exist: {kdk_path}") + logging.warning(f"KDK does not exist: {kdk_path}") return rm_args = ["rm", "-rf" if Path(kdk_path).is_dir() else "-f", kdk_path] result = utilities.elevated(rm_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.warning(f"- Failed to remove KDK: {kdk_path}") - logging.warning(f"- {result.stdout.decode('utf-8')}") + logging.warning(f"Failed to remove KDK: {kdk_path}") + logging.warning(f"{result.stdout.decode('utf-8')}") return - logging.info(f"- Successfully removed KDK: {kdk_path}") + logging.info(f"Successfully removed KDK: {kdk_path}") def _remove_unused_kdks(self, exclude_builds: list = None) -> None: @@ -495,7 +495,7 @@ class KernelDebugKitObject: if not Path(KDK_INSTALL_PATH).exists(): return - logging.info("- Cleaning unused KDKs") + logging.info("Cleaning unused KDKs") for kdk_folder in Path(KDK_INSTALL_PATH).iterdir(): if kdk_folder.name.endswith(".kdk") or kdk_folder.name.endswith(".pkg"): should_remove = True @@ -532,17 +532,17 @@ class KernelDebugKitObject: # TODO: should we use the checksum from the API? result = subprocess.run(["hdiutil", "verify", self.constants.kdk_download_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: - logging.info("- Error: Kernel Debug Kit checksum verification failed!") - logging.info(f"- Output: {result.stderr.decode('utf-8')}") + logging.info("Error: Kernel Debug Kit checksum verification failed!") + logging.info(f"Output: {result.stderr.decode('utf-8')}") msg = "Kernel Debug Kit checksum verification failed, please try again.\n\nIf this continues to fail, ensure you're downloading on a stable network connection (ie. Ethernet)" - logging.info(f"- {msg}") + logging.info(f"{msg}") self.error_msg = msg return False self._remove_unused_kdks() self.success = True - logging.info("- Kernel Debug Kit checksum verified") + logging.info("Kernel Debug Kit checksum verified") return True @@ -568,17 +568,17 @@ class KernelDebugKitUtilities: """ if os.getuid() != 0: - logging.warning("- Cannot install KDK, not running as root") + logging.warning("Cannot install KDK, not running as root") return False - logging.info(f"- Installing KDK package: {kdk_path.name}") - logging.info(f" - This may take a while...") + logging.info(f"Installing KDK package: {kdk_path.name}") + logging.info(f"- This may take a while...") # TODO: Check whether enough disk space is available result = utilities.elevated(["installer", "-pkg", kdk_path, "-target", "/"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.info("- Failed to install KDK:") + logging.info("Failed to install KDK:") logging.info(result.stdout.decode('utf-8')) if result.stderr: logging.info(result.stderr.decode('utf-8')) @@ -599,21 +599,21 @@ class KernelDebugKitUtilities: """ if os.getuid() != 0: - logging.warning("- Cannot install KDK, not running as root") + logging.warning("Cannot install KDK, not running as root") return False - logging.info(f"- Extracting downloaded KDK disk image") + logging.info(f"Extracting downloaded KDK disk image") with tempfile.TemporaryDirectory() as mount_point: result = subprocess.run(["hdiutil", "attach", kdk_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.info("- Failed to mount KDK:") + logging.info("Failed to mount KDK:") logging.info(result.stdout.decode('utf-8')) return False kdk_pkg_path = Path(f"{mount_point}/KernelDebugKit.pkg") if not kdk_pkg_path.exists(): - logging.warning("- Failed to find KDK package in DMG, likely corrupted!!!") + logging.warning("Failed to find KDK package in DMG, likely corrupted!!!") self._unmount_disk_image(mount_point) return False @@ -624,7 +624,7 @@ class KernelDebugKitUtilities: self._create_backup(kdk_pkg_path, Path(f"{kdk_path.parent}/{KDK_INFO_PLIST}")) self._unmount_disk_image(mount_point) - logging.info("- Successfully installed KDK") + logging.info("Successfully installed KDK") return True def _unmount_disk_image(self, mount_point) -> None: @@ -647,31 +647,31 @@ class KernelDebugKitUtilities: """ if not kdk_path.exists(): - logging.warning("- KDK does not exist, cannot create backup") + logging.warning("KDK does not exist, cannot create backup") return if not kdk_info_plist.exists(): - logging.warning("- KDK Info.plist does not exist, cannot create backup") + logging.warning("KDK Info.plist does not exist, cannot create backup") return kdk_info_dict = plistlib.load(kdk_info_plist.open("rb")) if 'version' not in kdk_info_dict or 'build' not in kdk_info_dict: - logging.warning("- Malformed KDK Info.plist provided, cannot create backup") + logging.warning("Malformed KDK Info.plist provided, cannot create backup") return if os.getuid() != 0: - logging.warning("- Cannot create KDK backup, not running as root") + logging.warning("Cannot create KDK backup, not running as root") return kdk_dst_name = f"KDK_{kdk_info_dict['version']}_{kdk_info_dict['build']}.pkg" kdk_dst_path = Path(f"{KDK_INSTALL_PATH}/{kdk_dst_name}") - logging.info(f"- Creating backup: {kdk_dst_name}") + logging.info(f"Creating backup: {kdk_dst_name}") if kdk_dst_path.exists(): - logging.info("- Backup already exists, skipping") + logging.info("Backup already exists, skipping") return result = utilities.elevated(["cp", "-R", kdk_path, kdk_dst_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.info("- Failed to create KDK backup:") + logging.info("Failed to create KDK backup:") logging.info(result.stdout.decode('utf-8')) \ No newline at end of file diff --git a/resources/logging_handler.py b/resources/logging_handler.py index 88cdc2022..242f70d20 100644 --- a/resources/logging_handler.py +++ b/resources/logging_handler.py @@ -1,5 +1,7 @@ import os import sys +import time +import pprint import logging import threading import traceback @@ -8,7 +10,7 @@ import applescript from pathlib import Path -from resources import constants +from resources import constants, analytics_handler class InitializeLoggingSupport: @@ -32,22 +34,23 @@ class InitializeLoggingSupport: """ def __init__(self, global_constants: constants.Constants) -> None: - self.log_filename: str = "OpenCore-Patcher.log" - self.log_filepath: Path = None - self.constants: constants.Constants = global_constants + self.log_filename: str = f"OpenCore-Patcher_{self.constants.patcher_version}_{time.strftime('%Y-%m-%d_%H-%M-%S')}.log" + self.log_filepath: Path = None + self.original_excepthook: sys = sys.excepthook self.original_thread_excepthook: threading = threading.excepthook - self.max_file_size: int = 1024 * 1024 * 10 # 10 MB - self.file_size_redline: int = 1024 * 1024 * 9 # 9 MB, when to start cleaning log file + self.max_file_size: int = 1024 * 1024 # 1 MB + self.file_size_redline: int = 1024 * 1024 - 1024 * 100 # 900 KB, when to start cleaning log file self._initialize_logging_path() - self._clean_log_file() self._attempt_initialize_logging_configuration() + self._start_logging() self._implement_custom_traceback_handler() self._fix_file_permission() + self._clean_prior_version_logs() def _initialize_logging_path(self) -> None: @@ -55,39 +58,57 @@ class InitializeLoggingSupport: Initialize logging framework storage path """ - self.log_filepath = Path(f"~/Library/Logs/{self.log_filename}").expanduser() + base_path = Path("~/Library/Logs").expanduser() + if not base_path.exists(): + # Likely in an installer environment, store in /Users/Shared + base_path = Path("/Users/Shared") + else: + # create Dortania folder if it doesn't exist + base_path = base_path / "Dortania" + if not base_path.exists(): + try: + base_path.mkdir() + except Exception as e: + logging.error(f"Failed to create Dortania folder: {e}") + base_path = Path("/Users/Shared") - if not self.log_filepath.parent.exists(): - # Likely in an installer environment, store in /Users/Shared - self.log_filepath = Path("/Users/Shared") / self.log_filename + self.log_filepath = Path(f"{base_path}/{self.log_filename}").expanduser() + self.constants.log_filepath = self.log_filepath - print("- Initializing logging framework...") - print(f" - Log file: {self.log_filepath}") - - - def _clean_log_file(self) -> None: + def _clean_prior_version_logs(self) -> None: """ - Determine if log file should be cleaned + Clean logs from old Patcher versions - We check if we're near the max file size, and if so, we clean the log file + Keep 10 latest logs """ - if not self.log_filepath.exists(): - return + paths = [ + self.log_filepath.parent, # ~/Library/Logs/Dortania + self.log_filepath.parent.parent, # ~/Library/Logs (old location) + ] - if self.log_filepath.stat().st_size < self.file_size_redline: - return + logs = [] - # Check if backup log file exists - backup_log_filepath = self.log_filepath.with_suffix(".old.log") - try: - if backup_log_filepath.exists(): - backup_log_filepath.unlink() + for path in paths: + for file in path.glob("OpenCore-Patcher*"): + if not file.is_file(): + continue - # Rename current log file to backup log file - self.log_filepath.rename(backup_log_filepath) - except Exception as e: - print(f"- Failed to clean log file: {e}") + if not file.name.endswith(".log"): + continue + + if file.name == self.log_filename: + continue + + logs.append(file) + + logs.sort(key=lambda x: x.stat().st_mtime, reverse=True) + + for log in logs[9:]: + try: + log.unlink() + except Exception as e: + logging.error(f"Failed to delete log file: {e}") def _fix_file_permission(self) -> None: @@ -101,11 +122,21 @@ class InitializeLoggingSupport: if os.geteuid() != 0: return - result = subprocess.run(["chmod", "777", self.log_filepath], capture_output=True) - if result.returncode != 0: - print(f"- Failed to fix log file permissions") - if result.stderr: - print(result.stderr.decode("utf-8")) + paths = [ + self.log_filepath, # ~/Library/Logs/Dortania/OpenCore-Patcher_{version}_{date}.log + self.log_filepath.parent, # ~/Library/Logs/Dortania + ] + + for path in paths: + result = subprocess.run(["chmod", "777", path], capture_output=True) + if result.returncode != 0: + logging.error(f"Failed to fix log file permissions") + if result.stdout: + logging.error("STDOUT:") + logging.error(result.stdout.decode("utf-8")) + if result.stderr: + logging.error("STDERR:") + logging.error(result.stderr.decode("utf-8")) def _initialize_logging_configuration(self, log_to_file: bool = True) -> None: @@ -122,7 +153,7 @@ class InitializeLoggingSupport: logging.basicConfig( level=logging.NOTSET, - format="%(asctime)s - %(filename)s (%(lineno)d): %(message)s", + format="[%(asctime)s] [%(filename)-32s] [%(lineno)-4d]: %(message)s", handlers=[ logging.StreamHandler(stream = sys.stdout), logging.FileHandler(self.log_filepath) if log_to_file is True else logging.NullHandler() @@ -143,11 +174,32 @@ class InitializeLoggingSupport: try: self._initialize_logging_configuration() except Exception as e: - print(f"- Failed to initialize logging framework: {e}") - print("- Retrying without logging to file...") + print(f"Failed to initialize logging framework: {e}") + print("Retrying without logging to file...") self._initialize_logging_configuration(log_to_file=False) + def _start_logging(self): + """ + Start logging, used as easily identifiable start point in logs + """ + + str_msg = f"# OpenCore Legacy Patcher ({self.constants.patcher_version}) #" + str_len = len(str_msg) + + logging.info('#' * str_len) + logging.info(str_msg) + logging.info('#' * str_len) + + logging.info("Log file set:") + # Display relative path to avoid disclosing user's username + try: + path = self.log_filepath.relative_to(Path.home()) + logging.info(f"~/{path}") + except ValueError: + logging.info(self.log_filepath) + + def _implement_custom_traceback_handler(self) -> None: """ Reroute traceback to logging module @@ -158,6 +210,7 @@ class InitializeLoggingSupport: Reroute traceback in main thread to logging module """ logging.error("Uncaught exception in main thread", exc_info=(type, value, tb)) + self._display_debug_properties() if self.constants.cli_mode is True: return @@ -166,9 +219,19 @@ class InitializeLoggingSupport: error_msg += f"{type.__name__}: {value}" if tb: error_msg += f"\n\n{traceback.extract_tb(tb)[-1]}" - error_msg += "\n\nPlease report this error on our Discord server." + error_msg += "\n\nSend crash report to Dortania?" - applescript.AppleScript(f'display dialog "{error_msg}" with title "OpenCore Legacy Patcher ({self.constants.patcher_version})" buttons {{"OK"}} default button "OK" with icon caution giving up after 30').run() + # Ask user if they want to send crash report + try: + result = applescript.AppleScript(f'display dialog "{error_msg}" with title "OpenCore Legacy Patcher ({self.constants.patcher_version})" buttons {{"Yes", "No"}} default button "Yes" with icon caution').run() + except Exception as e: + logging.error(f"Failed to display crash report dialog: {e}") + return + + if result[applescript.AEType(b'bhit')] != "Yes": + return + + threading.Thread(target=analytics_handler.Analytics(self.constants).send_crash_report, args=(self.log_filepath,)).start() def custom_thread_excepthook(args) -> None: @@ -188,3 +251,23 @@ class InitializeLoggingSupport: sys.excepthook = self.original_excepthook threading.excepthook = self.original_thread_excepthook + + + def _display_debug_properties(self) -> None: + """ + Display debug properties, primarily after main thread crash + """ + logging.info("Host Properties:") + logging.info(f" XNU Version: {self.constants.detected_os}.{self.constants.detected_os_minor}") + logging.info(f" XNU Build: {self.constants.detected_os_build}") + logging.info(f" macOS Version: {self.constants.detected_os_version}") + logging.info("Debug Properties:") + logging.info(f" Effective User ID: {os.geteuid()}") + logging.info(f" Effective Group ID: {os.getegid()}") + logging.info(f" Real User ID: {os.getuid()}") + logging.info(f" Real Group ID: {os.getgid()}") + logging.info(" Arguments passed to Patcher:") + for arg in sys.argv: + logging.info(f" {arg}") + + logging.info(f"Host Properties:\n{pprint.pformat(self.constants.computer.__dict__, indent=4)}") diff --git a/resources/macos_installer_handler.py b/resources/macos_installer_handler.py index 23b70f622..04a8f6bd2 100644 --- a/resources/macos_installer_handler.py +++ b/resources/macos_installer_handler.py @@ -53,7 +53,7 @@ class InstallerCreation(): bool: True if successful, False otherwise """ - logging.info("- Extracting macOS installer from InstallAssistant.pkg\n This may take some time") + logging.info("Extracting macOS installer from InstallAssistant.pkg") try: applescript.AppleScript( f'''do shell script "installer -pkg {Path(download_path)}/InstallAssistant.pkg -target /"''' @@ -62,11 +62,11 @@ class InstallerCreation(): " without altering line endings", ).run() except Exception as e: - logging.info("- Failed to install InstallAssistant") + logging.info("Failed to install InstallAssistant") logging.info(f" Error Code: {e}") return False - logging.info("- InstallAssistant installed") + logging.info("InstallAssistant installed") return True @@ -201,7 +201,6 @@ fi if not any(all_disks[disk]['removable'] is False for partition in all_disks[disk]): continue - logging.info(f"disk {disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})") list_disks.update({ disk: { "identifier": all_disks[disk]["identifier"], diff --git a/resources/main.py b/resources/main.py index 3041533bc..e0448c12e 100644 --- a/resources/main.py +++ b/resources/main.py @@ -32,8 +32,6 @@ class OpenCoreLegacyPatcher: logging_handler.InitializeLoggingSupport(self.constants) - logging.info(f"- Loading OpenCore Legacy Patcher v{self.constants.patcher_version}...") - self._generate_base_data() if utilities.check_cli_args() is None: @@ -94,14 +92,13 @@ class OpenCoreLegacyPatcher: # Generate defaults defaults.GenerateDefaults(self.computer.real_model, True, self.constants) - threading.Thread(target=analytics_handler.Analytics, args=(self.constants,)).start() + threading.Thread(target=analytics_handler.Analytics(self.constants).send_analytics).start() if utilities.check_cli_args() is None: self.constants.cli_mode = False - logging.info(f"- No arguments present, loading {'GUI' if self.constants.wxpython_variant is True else 'TUI'} mode") return - logging.info("- Detected arguments, switching to CLI mode") + logging.info("Detected arguments, switching to CLI mode") self.constants.gui_mode = True # Assumes no user interaction is required ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch", "--update_installed"] diff --git a/resources/network_handler.py b/resources/network_handler.py index 2dc61fd6f..a6029ce9d 100644 --- a/resources/network_handler.py +++ b/resources/network_handler.py @@ -204,10 +204,10 @@ class DownloadObject: """ self.status = DownloadStatus.DOWNLOADING - logging.info(f"- Starting download: {self.filename}") + logging.info(f"Starting download: {self.filename}") if spawn_thread: if self.active_thread: - logging.error("- Download already in progress") + logging.error("Download already in progress") return self.should_checksum = verify_checksum self.active_thread = threading.Thread(target=self._download, args=(display_progress,)) @@ -267,8 +267,8 @@ class DownloadObject: else: raise Exception("Content-Length missing from headers") except Exception as e: - logging.error(f"- Error determining file size {self.url}: {str(e)}") - logging.error("- Assuming file size is 0") + logging.error(f"Error determining file size {self.url}: {str(e)}") + logging.error("Assuming file size is 0") self.total_file_size = 0.0 @@ -295,17 +295,17 @@ class DownloadObject: try: if Path(path).exists(): - logging.info(f"- Deleting existing file: {path}") + logging.info(f"Deleting existing file: {path}") Path(path).unlink() return True if not Path(path).parent.exists(): - logging.info(f"- Creating directory: {Path(path).parent}") + logging.info(f"Creating directory: {Path(path).parent}") Path(path).parent.mkdir(parents=True, exist_ok=True) available_space = utilities.get_free_space(Path(path).parent) if self.total_file_size > available_space: - msg = f"- Not enough free space to download {self.filename}, need {utilities.human_fmt(self.total_file_size)}, have {utilities.human_fmt(available_space)}" + msg = f"Not enough free space to download {self.filename}, need {utilities.human_fmt(self.total_file_size)}, have {utilities.human_fmt(available_space)}" logging.error(msg) raise Exception(msg) @@ -313,7 +313,7 @@ class DownloadObject: self.error = True self.error_msg = str(e) self.status = DownloadStatus.ERROR - logging.error(f"- Error preparing working directory {path}: {self.error_msg}") + logging.error(f"Error preparing working directory {path}: {self.error_msg}") return False logging.info(f"- Directory ready: {path}") @@ -354,21 +354,21 @@ class DownloadObject: if display_progress and i % 100: # Don't use logging here, as we'll be spamming the log file if self.total_file_size == 0.0: - print(f"- Downloaded {utilities.human_fmt(self.downloaded_file_size)} of {self.filename}") + print(f"Downloaded {utilities.human_fmt(self.downloaded_file_size)} of {self.filename}") else: - print(f"- Downloaded {self.get_percent():.2f}% of {self.filename} ({utilities.human_fmt(self.get_speed())}/s) ({self.get_time_remaining():.2f} seconds remaining)") + print(f"Downloaded {self.get_percent():.2f}% of {self.filename} ({utilities.human_fmt(self.get_speed())}/s) ({self.get_time_remaining():.2f} seconds remaining)") self.download_complete = True - logging.info(f"- Download complete: {self.filename}") - logging.info("- Stats:") - logging.info(f"- Downloaded size: {utilities.human_fmt(self.downloaded_file_size)}") - logging.info(f"- Time elapsed: {(time.time() - self.start_time):.2f} seconds") - logging.info(f"- Speed: {utilities.human_fmt(self.downloaded_file_size / (time.time() - self.start_time))}/s") - logging.info(f"- Location: {self.filepath}") + logging.info(f"Download complete: {self.filename}") + logging.info("Stats:") + logging.info(f"- Downloaded size: {utilities.human_fmt(self.downloaded_file_size)}") + logging.info(f"- Time elapsed: {(time.time() - self.start_time):.2f} seconds") + logging.info(f"- Speed: {utilities.human_fmt(self.downloaded_file_size / (time.time() - self.start_time))}/s") + logging.info(f"- Location: {self.filepath}") except Exception as e: self.error = True self.error_msg = str(e) self.status = DownloadStatus.ERROR - logging.error(f"- Error downloading {self.url}: {self.error_msg}") + logging.error(f"Error downloading {self.url}: {self.error_msg}") self.status = DownloadStatus.COMPLETE utilities.enable_sleep_after_running() diff --git a/resources/reroute_payloads.py b/resources/reroute_payloads.py index 760e359fb..2272b5fd7 100644 --- a/resources/reroute_payloads.py +++ b/resources/reroute_payloads.py @@ -28,10 +28,10 @@ class RoutePayloadDiskImage: """ if self.constants.wxpython_variant is True and not self.constants.launcher_script: - logging.info("- Running in Binary GUI mode, switching to tmp directory") + logging.info("Running in Binary GUI mode, switching to tmp directory") self.temp_dir = tempfile.TemporaryDirectory() - logging.info(f"- New payloads location: {self.temp_dir.name}") - logging.info("- Creating payloads directory") + logging.info(f"New payloads location: {self.temp_dir.name}") + logging.info("Creating payloads directory") Path(self.temp_dir.name / Path("payloads")).mkdir(parents=True, exist_ok=True) self._unmount_active_dmgs(unmount_all_active=False) output = subprocess.run( @@ -45,12 +45,12 @@ class RoutePayloadDiskImage: stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) if output.returncode == 0: - logging.info("- Mounted payloads.dmg") + logging.info("Mounted payloads.dmg") self.constants.current_path = Path(self.temp_dir.name) self.constants.payload_path = Path(self.temp_dir.name) / Path("payloads") atexit.register(self._unmount_active_dmgs, unmount_all_active=False) else: - logging.info("- Failed to mount payloads.dmg") + logging.info("Failed to mount payloads.dmg") logging.info(f"Output: {output.stdout.decode()}") logging.info(f"Return Code: {output.returncode}") @@ -78,13 +78,13 @@ class RoutePayloadDiskImage: # Check that only our personal payloads.dmg is unmounted if "shadow-path" in image: if self.temp_dir.name in image["shadow-path"]: - logging.info(f"- Unmounting personal {variant}") + logging.info(f"Unmounting personal {variant}") subprocess.run( ["hdiutil", "detach", image["system-entities"][0]["dev-entry"], "-force"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) else: - logging.info(f"- Unmounting {variant} at: {image['system-entities'][0]['dev-entry']}") + logging.info(f"Unmounting {variant} at: {image['system-entities'][0]['dev-entry']}") subprocess.run( ["hdiutil", "detach", image["system-entities"][0]["dev-entry"], "-force"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT diff --git a/resources/sys_patch/sys_patch_detect.py b/resources/sys_patch/sys_patch_detect.py index 9a2752efe..2766c0cd5 100644 --- a/resources/sys_patch/sys_patch_detect.py +++ b/resources/sys_patch/sys_patch_detect.py @@ -92,7 +92,7 @@ class DetectRootPatch: non_metal_os = os_data.os_data.catalina for i, gpu in enumerate(gpus): if gpu.class_code and gpu.class_code != 0xFFFFFFFF: - logging.info(f"- Found GPU ({i}): {utilities.friendly_hex(gpu.vendor_id)}:{utilities.friendly_hex(gpu.device_id)}") + logging.info(f"Found GPU ({i}): {utilities.friendly_hex(gpu.vendor_id)}:{utilities.friendly_hex(gpu.device_id)}") if gpu.arch in [device_probe.NVIDIA.Archs.Tesla] and self.constants.force_nv_web is False: if self.constants.detected_os > non_metal_os: self.nvidia_tesla = True diff --git a/resources/sys_patch/sys_patch_generate.py b/resources/sys_patch/sys_patch_generate.py index 4443e45fa..ebb0f5c89 100644 --- a/resources/sys_patch/sys_patch_generate.py +++ b/resources/sys_patch/sys_patch_generate.py @@ -40,7 +40,7 @@ class GenerateRootPatchSets: utilities.cls() - logging.info("- The following patches will be applied:") + logging.info("The following patches will be applied:") if self.hardware_details["Graphics: Intel Ironlake"] is True: required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]}) @@ -184,8 +184,8 @@ class GenerateRootPatchSets: del(required_patches[patch_name]) else: if required_patches[patch_name]["Display Name"]: - logging.info(f" - {required_patches[patch_name]['Display Name']}") + logging.info(f"- {required_patches[patch_name]['Display Name']}") else: - logging.info(" - No patch sets found for booted model") + logging.info("- No patch sets found for booted model") return required_patches \ No newline at end of file diff --git a/resources/sys_patch/sys_patch_helpers.py b/resources/sys_patch/sys_patch_helpers.py index ec8504b6c..816dfb855 100644 --- a/resources/sys_patch/sys_patch_helpers.py +++ b/resources/sys_patch/sys_patch_helpers.py @@ -42,10 +42,10 @@ class SysPatchHelpers: if self.constants.computer.reported_board_id in self.constants.sandy_board_id_stock: return - logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching") + logging.info(f"Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching") board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus) - logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}") + logging.info(f"Replacing {board_to_patch} with {self.constants.computer.reported_board_id}") board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex()) reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex()) @@ -54,12 +54,12 @@ class SysPatchHelpers: # Pad the reported Board ID with zeros to match the length of the board to patch reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex)) elif len(board_to_patch_hex) < len(reported_board_hex): - logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}") + logging.info(f"Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}") raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!") path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB" if not Path(path).exists(): - logging.info(f"- Error: Could not find {path}") + logging.info(f"Error: Could not find {path}") raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!") with open(path, 'rb') as f: @@ -128,7 +128,7 @@ class SysPatchHelpers: if self.constants.detected_os < os_data.os_data.ventura: return - logging.info("- Disabling WindowServer Caching") + logging.info("Disabling WindowServer Caching") # Invoke via 'bash -c' to resolve pathing utilities.elevated(["bash", "-c", "rm -rf /private/var/folders/*/*/*/WindowServer/com.apple.WindowServer"]) # Disable writing to WindowServer folder @@ -150,12 +150,12 @@ class SysPatchHelpers: if self.constants.detected_os < os_data.os_data.ventura: return - logging.info("- Parsing Notification Centre Widgets") + logging.info("Parsing Notification Centre Widgets") file_path = "~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist" file_path = Path(file_path).expanduser() if not file_path.exists(): - logging.info(" - Defaults file not found, skipping") + logging.info("- Defaults file not found, skipping") return did_find = False @@ -178,7 +178,7 @@ class SysPatchHelpers: continue if not b'com.apple.news' in sub_data[sub_entry][2]: continue - logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}") + logging.info(f"- Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}") data["widgets"]["instances"].remove(widget) did_find = True @@ -210,10 +210,10 @@ class SysPatchHelpers: if self.constants.detected_os < os_data.os_data.big_sur: return - logging.info("- Installing Kernel Collection syncing utility") + logging.info("Installing Kernel Collection syncing utility") result = utilities.elevated([self.constants.rsrrepair_userspace_path, "--install"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.info(f" - Failed to install RSRRepair: {result.stdout.decode()}") + logging.info(f"- Failed to install RSRRepair: {result.stdout.decode()}") def patch_gpu_compiler_libraries(self, mount_point: Union[str, Path]): @@ -262,7 +262,7 @@ class SysPatchHelpers: if not file.name.startswith("31001."): continue - logging.info(f"- Merging GPUCompiler.framework libraries to match binary") + logging.info(f"Merging GPUCompiler.framework libraries to match binary") src_dir = f"{LIBRARY_DIR}/{file.name}" if not Path(f"{DEST_DIR}/lib").exists(): diff --git a/resources/updates.py b/resources/updates.py index 850a1b0e6..8b0df26c8 100644 --- a/resources/updates.py +++ b/resources/updates.py @@ -116,7 +116,7 @@ class CheckBinaryUpdates: return None for asset in data_set["assets"]: - logging.info(f"- Found asset: {asset['name']}") + logging.info(f"Found asset: {asset['name']}") if self._determine_remote_type(asset["name"]) == self._determine_local_build_type(): available_binaries.update({ asset['name']: { diff --git a/resources/utilities.py b/resources/utilities.py index 63d3fad97..e7f476c70 100644 --- a/resources/utilities.py +++ b/resources/utilities.py @@ -158,7 +158,7 @@ sleep_process = None def disable_sleep_while_running(): global sleep_process - logging.info("- Disabling Idle Sleep") + logging.info("Disabling Idle Sleep") if sleep_process is None: # If sleep_process is active, we'll just keep it running sleep_process = subprocess.Popen(["caffeinate", "-d", "-i", "-s"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -168,7 +168,7 @@ def disable_sleep_while_running(): def enable_sleep_after_running(): global sleep_process if sleep_process: - logging.info("- Re-enabling Idle Sleep") + logging.info("Re-enabling Idle Sleep") sleep_process.kill() sleep_process = None @@ -394,9 +394,6 @@ def get_firmware_vendor(*, decode: bool = False): value = value.strip("\0") return value -def dump_constants(constants): - with open(os.path.join(os.path.expanduser('~'), 'Desktop', 'internal_data.txt'), 'w') as f: - f.write(str(vars(constants))) def find_apfs_physical_volume(device): # ex: disk3s1s1 @@ -516,7 +513,7 @@ def block_os_updaters(): for bad_process in bad_processes: if bad_process in current_process: if pid != "": - logging.info(f"- Killing Process: {pid} - {current_process.split('/')[-1]}") + logging.info(f"Killing Process: {pid} - {current_process.split('/')[-1]}") subprocess.run(["kill", "-9", pid]) break diff --git a/resources/validation.py b/resources/validation.py index b0b8fee7c..4d9ff59de 100644 --- a/resources/validation.py +++ b/resources/validation.py @@ -120,7 +120,7 @@ class PatcherValidation: logging.info(f"File not found: {source_file}") raise Exception(f"Failed to find {source_file}") - logging.info(f"- Validating against Darwin {major_kernel}.{minor_kernel}") + logging.info(f"Validating against Darwin {major_kernel}.{minor_kernel}") if not sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None): raise Exception("Failed to generate patchset plist") @@ -153,13 +153,13 @@ class PatcherValidation: ) if output.returncode != 0: - logging.info("- Failed to mount Universal-Binaries.dmg") + logging.info("Failed to mount Universal-Binaries.dmg") logging.info(f"Output: {output.stdout.decode()}") logging.info(f"Return Code: {output.returncode}") raise Exception("Failed to mount Universal-Binaries.dmg") - logging.info("- Mounted Universal-Binaries.dmg") + logging.info("Mounted Universal-Binaries.dmg") for supported_os in [os_data.os_data.big_sur, os_data.os_data.monterey, os_data.os_data.ventura]: @@ -179,7 +179,7 @@ class PatcherValidation: ) if output.returncode != 0: - logging.info("- Failed to unmount Universal-Binaries.dmg") + logging.info("Failed to unmount Universal-Binaries.dmg") logging.info(f"Output: {output.stdout.decode()}") logging.info(f"Return Code: {output.returncode}") diff --git a/resources/wx_gui/gui_about.py b/resources/wx_gui/gui_about.py index f73cd29cf..73b0c84a1 100644 --- a/resources/wx_gui/gui_about.py +++ b/resources/wx_gui/gui_about.py @@ -2,6 +2,9 @@ import wx import wx.adv +import logging + +from resources.wx_gui import gui_support from resources import constants @@ -12,6 +15,7 @@ class AboutFrame(wx.Frame): if wx.FindWindowByName("About"): return + logging.info("Generating About frame") super(AboutFrame, self).__init__(None, title="About", size=(350, 350), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) self.constants: constants.Constants = global_constants self.Centre() diff --git a/resources/wx_gui/gui_build.py b/resources/wx_gui/gui_build.py index a9a6487b0..5dafddf26 100644 --- a/resources/wx_gui/gui_build.py +++ b/resources/wx_gui/gui_build.py @@ -19,6 +19,7 @@ class BuildFrame(wx.Frame): 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) -> None: + logging.info("Initializing Build Frame") super(BuildFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) gui_support.GenerateMenubar(self, global_constants).generate() @@ -121,7 +122,7 @@ class BuildFrame(wx.Frame): try: build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants) except: - logging.error("- An internal error occurred while building:\n") + logging.error("An internal error occurred while building:\n") logging.error(traceback.format_exc()) logger.removeHandler(logger.handlers[2]) diff --git a/resources/wx_gui/gui_download.py b/resources/wx_gui/gui_download.py index 1f15cecec..a926f8570 100644 --- a/resources/wx_gui/gui_download.py +++ b/resources/wx_gui/gui_download.py @@ -1,5 +1,6 @@ # Generate UI for downloading files import wx +import logging from resources import ( constants, @@ -13,6 +14,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) -> None: + logging.info("Initializing Download Frame") self.constants: constants.Constants = global_constants self.title: str = title self.parent: wx.Frame = parent @@ -91,6 +93,7 @@ class DownloadFrame(wx.Frame): Terminate download """ if wx.MessageBox("Are you sure you want to cancel the download?", "Cancel Download", wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT) == wx.YES: + logging.info("User cancelled download") self.user_cancelled = True self.download_obj.stop() diff --git a/resources/wx_gui/gui_entry.py b/resources/wx_gui/gui_entry.py index 49a92605e..c2bcda0ff 100644 --- a/resources/wx_gui/gui_entry.py +++ b/resources/wx_gui/gui_entry.py @@ -9,8 +9,7 @@ from resources.wx_gui import ( gui_main_menu, gui_build, gui_install_oc, - gui_sys_patch, - gui_support, + gui_sys_patch_start, gui_update, ) from resources.sys_patch import sys_patch_detect @@ -23,7 +22,7 @@ class SupportedEntryPoints: MAIN_MENU = gui_main_menu.MainFrame BUILD_OC = gui_build.BuildFrame INSTALL_OC = gui_install_oc.InstallOCFrame - SYS_PATCH = gui_sys_patch.SysPatchFrame + SYS_PATCH = gui_sys_patch_start.SysPatchStartFrame UPDATE_APP = gui_update.UpdateFrame @@ -49,10 +48,10 @@ class EntryPoint: self._generate_base_data() if "--gui_patch" in sys.argv or "--gui_unpatch" in sys.argv: - entry = gui_sys_patch.SysPatchFrame + entry = gui_sys_patch_start.SysPatchStartFrame patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() - logging.info(f"- Loading wxPython GUI: {entry.__name__}") + logging.info(f"Entry point set: {entry.__name__}") self.frame: wx.Frame = entry( None, title=f"{self.constants.patcher_name} ({self.constants.patcher_version})", @@ -64,9 +63,9 @@ class EntryPoint: atexit.register(self.OnCloseFrame) if "--gui_patch" in sys.argv: - self.frame.start_root_patching(patches) + self.frame.start_root_patching() elif "--gui_unpatch" in sys.argv: - self.frame.revert_root_patching(patches) + self.frame.revert_root_patching() self.app.MainLoop() @@ -79,7 +78,7 @@ class EntryPoint: if not self.frame: return - logging.info("- Cleaning up wxPython GUI") + logging.info("Cleaning up wxPython GUI") self.frame.SetTransparent(0) wx.Yield() diff --git a/resources/wx_gui/gui_help.py b/resources/wx_gui/gui_help.py index b5c880ed0..4f0871a7f 100644 --- a/resources/wx_gui/gui_help.py +++ b/resources/wx_gui/gui_help.py @@ -1,5 +1,6 @@ # Generate UI for help menu import wx +import logging import webbrowser from resources import constants @@ -10,7 +11,7 @@ class HelpFrame(wx.Frame): Append to main menu through a modal dialog """ def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None) -> None: - + logging.info("Initializing Help Frame") self.dialog = wx.Dialog(parent, title=title, size=(300, 200)) self.constants: constants.Constants = global_constants diff --git a/resources/wx_gui/gui_install_oc.py b/resources/wx_gui/gui_install_oc.py index 3a2c9d0f9..70c2cbcb0 100644 --- a/resources/wx_gui/gui_install_oc.py +++ b/resources/wx_gui/gui_install_oc.py @@ -3,7 +3,7 @@ import threading import logging import traceback -from resources.wx_gui import gui_main_menu, gui_support, gui_sys_patch +from resources.wx_gui import gui_main_menu, gui_support, gui_sys_patch_display from resources import constants, install from data import os_data @@ -13,6 +13,7 @@ class InstallOCFrame(wx.Frame): Create a frame for installing OpenCore to disk """ def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + logging.info("Initializing Install OpenCore Frame") super(InstallOCFrame, self).__init__(parent, title=title, size=(300, 120), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) gui_support.GenerateMenubar(self, global_constants).generate() @@ -124,13 +125,14 @@ class InstallOCFrame(wx.Frame): # disk6s1 -> disk6 disk_root = self.constants.booted_oc_disk.strip("disk") disk_root = "disk" + disk_root.split("s")[0] - logging.info(f"- Checking if booted disk is present: {disk_root}") + logging.info(f"Checking if booted disk is present: {disk_root}") # Add buttons for each disk items = len(self.available_disks) longest_label = max((len(self.available_disks[disk]['disk']) + len(self.available_disks[disk]['name']) + len(str(self.available_disks[disk]['size']))) for disk in self.available_disks) longest_label = longest_label * 9 spacer = 0 + logging.info("Available disks:") for disk in self.available_disks: # Create a button for each disk logging.info(f"- {self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}") @@ -196,6 +198,7 @@ class InstallOCFrame(wx.Frame): longest_label = max((len(partitions[partition]['partition']) + len(partitions[partition]['name']) + len(str(partitions[partition]['size']))) for partition in partitions) longest_label = longest_label * 10 spacer = 0 + logging.info(f"Available partitions for {disk}:") for partition in partitions: logging.info(f"- {partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}") disk_button = wx.Button(dialog, label=f"{partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}", size=(longest_label,30), pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5 + spacer)) @@ -281,7 +284,7 @@ class InstallOCFrame(wx.Frame): popup_message.ShowModal() if popup_message.GetReturnCode() == wx.ID_YES: self.Hide() - gui_sys_patch.SysPatchFrame( + gui_sys_patch_display.SysPatchDisplayFrame( parent=None, title=self.title, global_constants=self.constants, @@ -308,14 +311,14 @@ class InstallOCFrame(wx.Frame): """ Install OpenCore to disk """ - logging.info(f"- Installing OpenCore to {partition}") + logging.info(f"Installing OpenCore to {partition}") logger = logging.getLogger() logger.addHandler(gui_support.ThreadHandler(self.text_box)) try: self.result = install.tui_disk_installation(self.constants).install_opencore(partition) except: - logging.error("- An internal error occurred while installing:\n") + logging.error("An internal error occurred while installing:\n") logging.error(traceback.format_exc()) logger.removeHandler(logger.handlers[2]) diff --git a/resources/wx_gui/gui_macos_installer_download.py b/resources/wx_gui/gui_macos_installer_download.py index 1dc261176..adc569164 100644 --- a/resources/wx_gui/gui_macos_installer_download.py +++ b/resources/wx_gui/gui_macos_installer_download.py @@ -28,7 +28,7 @@ class macOSInstallerDownloadFrame(wx.Frame): Note: Flashing installers is passed to gui_macos_installer_flash.py """ def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): - + logging.info("Initializing macOS Installer Download Frame") self.constants: constants.Constants = global_constants self.title: str = title self.parent: wx.Frame = parent @@ -36,6 +36,8 @@ class macOSInstallerDownloadFrame(wx.Frame): self.available_installers = None self.available_installers_latest = None + self.catalog_seed: macos_installer_handler.SeedType = macos_installer_handler.SeedType.DeveloperSeed + self.frame_modal = wx.Dialog(parent, title=title, size=(330, 200)) self._generate_elements(self.frame_modal) @@ -102,7 +104,8 @@ class macOSInstallerDownloadFrame(wx.Frame): # Grab installer catalog def _fetch_installers(): - remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=macos_installer_handler.SeedType.DeveloperSeed) + logging.info(f"Fetching installer catalog: {macos_installer_handler.SeedType(self.catalog_seed).name}") + remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=self.catalog_seed) self.available_installers = remote_obj.available_apps self.available_installers_latest = remote_obj.available_apps_latest @@ -138,8 +141,9 @@ class macOSInstallerDownloadFrame(wx.Frame): installers = self.available_installers_latest if show_full is False else self.available_installers if installers: spacer = 0 + logging.info(f"Available installers on SUCatalog ({'All entries' if show_full else 'Latest only'}):") for app in installers: - logging.info(f"macOS {installers[app]['Version']} ({installers[app]['Build']}):\n - Size: {utilities.human_fmt(installers[app]['Size'])}\n - Source: {installers[app]['Source']}\n - Variant: {installers[app]['Variant']}\n - Link: {installers[app]['Link']}\n") + logging.info(f"- macOS {installers[app]['Version']} ({installers[app]['Build']}):\n - Size: {utilities.human_fmt(installers[app]['Size'])}\n - Source: {installers[app]['Source']}\n - Variant: {installers[app]['Variant']}\n - Link: {installers[app]['Link']}\n") extra = " Beta" if installers[app]['Variant'] in ["DeveloperSeed" , "PublicSeed"] else "" installer_button = wx.Button(dialog, label=f"macOS {installers[app]['Version']}{extra} ({installers[app]['Build']} - {utilities.human_fmt(installers[app]['Size'])})", pos=(-1, subtitle_label.GetPosition()[1] + subtitle_label.GetSize()[1] + 5 + spacer), size=(270, 30)) @@ -172,6 +176,7 @@ class macOSInstallerDownloadFrame(wx.Frame): """ Download macOS installer """ + logging.info(f"Selected macOS {app['Version']} ({app['Build']})") # Notify user whether their model is compatible with the selected installer problems = [] @@ -185,6 +190,7 @@ class macOSInstallerDownloadFrame(wx.Frame): problems.append("Lack of internal Keyboard/Mouse in macOS installer.") if problems: + logging.warning(f"Potential issues with {model} and {app['Version']} ({app['Build']}): {problems}") problems = "\n".join(problems) dlg = wx.MessageDialog(self.frame_modal, f"Your model ({model}) may not be fully supported by this installer. You may encounter the following issues:\n\n{problems}\n\nFor more information, see associated page. Otherwise, we recommend using macOS Monterey", "Potential Issues", wx.YES_NO | wx.CANCEL | wx.ICON_WARNING) dlg.SetYesNoCancelLabels("View Github Issue", "Download Anyways", "Cancel") @@ -198,6 +204,7 @@ class macOSInstallerDownloadFrame(wx.Frame): host_space = utilities.get_free_space() needed_space = app['Size'] * 2 if host_space < needed_space: + logging.error(f"Insufficient space to download and extract: {utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required") dlg = wx.MessageDialog(self.frame_modal, f"You do not have enough free space to download and extract this installer. Please free up some space and try again\n\n{utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required", "Insufficient Space", wx.OK | wx.ICON_WARNING) dlg.ShowModal() return @@ -249,6 +256,7 @@ class macOSInstallerDownloadFrame(wx.Frame): chunklist_stream = network_handler.NetworkUtilities().get(chunklist_link).content if chunklist_stream: + logging.info("Validating macOS installer") utilities.disable_sleep_while_running() chunk_obj = integrity_verification.ChunklistVerification(self.constants.payload_path / Path("InstallAssistant.pkg"), chunklist_stream) if chunk_obj.chunks: @@ -265,10 +273,13 @@ class macOSInstallerDownloadFrame(wx.Frame): wx.App.Get().Yield() if chunk_obj.status == integrity_verification.ChunklistStatus.FAILURE: - wx.MessageBox("Chunklist validation failed.\n\nThis generally happens when downloading on unstable connections such as WiFi or cellular.\n\nPlease try redownloading again on a stable connection (ie. Ethernet)", "Corrupted Installer!", wx.OK | wx.ICON_ERROR) + logging.error(f"Chunklist validation failed: Hash mismatch on {chunk_obj.current_chunk}") + wx.MessageBox(f"Chunklist validation failed: Hash mismatch on {chunk_obj.current_chunk}\n\nThis generally happens when downloading on unstable connections such as WiFi or cellular.\n\nPlease try redownloading again on a stable connection (ie. Ethernet)", "Corrupted Installer!", wx.OK | wx.ICON_ERROR) self.on_return_to_main_menu() return + logging.info("macOS installer validated") + # Extract installer title_label.SetLabel("Extracting macOS Installer") title_label.Centre(wx.HORIZONTAL) diff --git a/resources/wx_gui/gui_macos_installer_flash.py b/resources/wx_gui/gui_macos_installer_flash.py index adfce3df3..288052083 100644 --- a/resources/wx_gui/gui_macos_installer_flash.py +++ b/resources/wx_gui/gui_macos_installer_flash.py @@ -22,6 +22,7 @@ from data import os_data class macOSInstallerFlashFrame(wx.Frame): def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + logging.info("Initializing macOS Installer Flash Frame") super(macOSInstallerFlashFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) gui_support.GenerateMenubar(self, global_constants).generate() @@ -78,7 +79,6 @@ class macOSInstallerFlashFrame(wx.Frame): wx.Yield() frame_modal = wx.Dialog(self, title=self.title, size=(350, 200)) - frame_modal.Centre(wx.HORIZONTAL) # Title: Select macOS Installer title_label = wx.StaticText(frame_modal, label="Select local macOS Installer", pos=(-1,5)) @@ -131,6 +131,7 @@ class macOSInstallerFlashFrame(wx.Frame): def on_select(self, installer: dict) -> None: + logging.info(f"Selected installer: {installer['Short Name']} ({installer['Version']} ({installer['Build']}))") self.frame_modal.Destroy() for child in self.GetChildren(): @@ -185,8 +186,9 @@ class macOSInstallerFlashFrame(wx.Frame): if self.available_disks: spacer = 5 entries = len(self.available_disks) + logging.info("Available disks:") for disk in self.available_disks: - logging.info(f"{disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}") + logging.info(f" - {disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}") disk_button = wx.Button(self.frame_modal, label=f"{disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + spacer), size=(300, 30)) disk_button.Bind(wx.EVT_BUTTON, lambda event, temp=disk: self.on_select_disk(self.available_disks[temp], installer)) disk_button.Centre(wx.HORIZONTAL) @@ -221,6 +223,8 @@ class macOSInstallerFlashFrame(wx.Frame): if answer != wx.YES: return + logging.info(f"Selected disk: {disk['name']}") + self.frame_modal.Destroy() for child in self.GetChildren(): @@ -261,6 +265,7 @@ class macOSInstallerFlashFrame(wx.Frame): # Prepare resources if self._prepare_resources(installer['Path'], disk['identifier']) is False: + logging.error("Failed to prepare resources, cannot continue.") wx.MessageBox("Failed to prepare resources, cannot continue.", "Error", wx.OK | wx.ICON_ERROR) self.on_return_to_main_menu() return @@ -280,6 +285,7 @@ class macOSInstallerFlashFrame(wx.Frame): initial_bytes_written = float(utilities.monitor_disk_output(root_disk)) self.result = False def _flash(): + logging.info(f"Flashing {installer['Path']} to {root_disk}") self.result = self._flash_installer(root_disk) thread = threading.Thread(target=_flash) @@ -293,10 +299,15 @@ class macOSInstallerFlashFrame(wx.Frame): total_bytes_written = initial_bytes_written bytes_written = total_bytes_written - initial_bytes_written wx.CallAfter(bytes_written_label.SetLabel, f"Bytes Written: {bytes_written:.2f} MB") - wx.CallAfter(progress_bar.SetValue, int(bytes_written)) + try: + bytes_written = int(bytes_written) + except: + bytes_written = 0 + wx.CallAfter(progress_bar.SetValue, bytes_written) wx.Yield() if self.result is False: + logging.error("Failed to flash installer, cannot continue.") self.on_return_to_main_menu() return @@ -354,14 +365,14 @@ class macOSInstallerFlashFrame(wx.Frame): def _flash_installer(self, disk) -> bool: utilities.disable_sleep_while_running() - logging.info("- Creating macOS installer") + logging.info("Creating macOS installer") thread = threading.Thread(target=self._auto_package_handler) thread.start() # print contents of installer.sh with open(self.constants.installer_sh_path, "r") as f: - logging.info(f"- installer.sh contents:\n{f.read()}") + logging.info(f"installer.sh contents:\n{f.read()}") args = [self.constants.oclp_helper_path, "/bin/sh", self.constants.installer_sh_path] result = subprocess.run(args, capture_output=True, text=True) @@ -369,17 +380,17 @@ class macOSInstallerFlashFrame(wx.Frame): error = result.stderr if result.stderr else "" if "Install media now available at" not in output: - logging.info("- Failed to create macOS installer") + logging.info("Failed to create macOS installer") popup = wx.MessageDialog(self, f"Failed to create macOS installer\n\nOutput: {output}\n\nError: {error}", "Error", wx.OK | wx.ICON_ERROR) popup.ShowModal() return False - logging.info("- Successfully created macOS installer") + logging.info("Successfully created macOS installer") while thread.is_alive(): # wait for download_thread to finish # though highly unlikely this thread is still alive (flashing an Installer will take a while) time.sleep(0.1) - logging.info("- Installing Root Patcher to drive") + logging.info("Installing Root Patcher to drive") self._install_installer_pkg(disk) utilities.enable_sleep_after_running() @@ -396,7 +407,7 @@ class macOSInstallerFlashFrame(wx.Frame): """ link = self.constants.installer_pkg_url if network_handler.NetworkUtilities(link).validate_link() is False: - logging.info("- Stock Install.pkg is missing on Github, falling back to Nightly") + logging.info("Stock Install.pkg is missing on Github, falling back to Nightly") link = self.constants.installer_pkg_url_nightly if link.endswith(".zip"): @@ -408,7 +419,7 @@ class macOSInstallerFlashFrame(wx.Frame): autopkg_download.download(spawn_thread=False) if autopkg_download.download_complete is False: - logging.warning("- Failed to download Install.pkg") + logging.warning("Failed to download Install.pkg") logging.warning(autopkg_download.error_msg) return @@ -434,7 +445,7 @@ class macOSInstallerFlashFrame(wx.Frame): os_version = plistlib.load(Path(path + "/System/Library/CoreServices/SystemVersion.plist").open("rb")) kernel_version = os_data.os_conversion.os_to_kernel(os_version["ProductVersion"]) if int(kernel_version) < os_data.os_data.big_sur: - logging.info("- Installer unsupported, requires Big Sur or newer") + logging.info("Installer unsupported, requires Big Sur or newer") return subprocess.run(["mkdir", "-p", f"{path}/Library/Packages/"]) @@ -460,29 +471,29 @@ class macOSInstallerFlashFrame(wx.Frame): if kdk_pkg_path.exists(): kdk_pkg_path.unlink() - logging.info("- Initiating KDK download") - logging.info(f" - Build: {build}") - logging.info(f" - Version: {version}") - logging.info(f" - Working Directory: {download_dir}") + logging.info("Initiating KDK download") + logging.info(f"- Build: {build}") + logging.info(f"- Version: {version}") + logging.info(f"- Working Directory: {download_dir}") kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, build, version, ignore_installed=True) if kdk_obj.success is False: - logging.info("- Failed to retrieve KDK") + logging.info("Failed to retrieve KDK") logging.info(kdk_obj.error_msg) return kdk_download_obj = kdk_obj.retrieve_download(override_path=kdk_dmg_path) if kdk_download_obj is None: - logging.info("- Failed to retrieve KDK") + logging.info("Failed to retrieve KDK") logging.info(kdk_obj.error_msg) # Check remaining disk space before downloading space = utilities.get_free_space(download_dir) if space < (kdk_obj.kdk_url_expected_size * 2): - logging.info("- Not enough disk space to download and install KDK") - logging.info(f"- Attempting to download locally first") + logging.info("Not enough disk space to download and install KDK") + logging.info(f"Attempting to download locally first") if space < kdk_obj.kdk_url_expected_size: - logging.info("- Not enough disk space to install KDK, skipping") + logging.info("Not enough disk space to install KDK, skipping") return # Ideally we'd download the KDK onto the disk to display progress in the UI # However we'll just download to our temp directory and move it to the target disk @@ -490,43 +501,46 @@ class macOSInstallerFlashFrame(wx.Frame): kdk_download_obj.download(spawn_thread=False) if kdk_download_obj.download_complete is False: - logging.info("- Failed to download KDK") + logging.info("Failed to download KDK") logging.info(kdk_download_obj.error_msg) return if not kdk_dmg_path.exists(): - logging.info(f"- KDK missing: {kdk_dmg_path}") + logging.info(f"KDK missing: {kdk_dmg_path}") return # Now that we have a KDK, extract it to get the pkg with tempfile.TemporaryDirectory() as mount_point: - logging.info("- Mounting KDK") + logging.info("Mounting KDK") result = subprocess.run(["hdiutil", "attach", kdk_dmg_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.info("- Failed to mount KDK") + logging.info("Failed to mount KDK") logging.info(result.stdout.decode("utf-8")) return - logging.info("- Copying KDK") + logging.info("Copying KDK") subprocess.run(["cp", "-r", f"{mount_point}/KernelDebugKit.pkg", kdk_pkg_path]) - logging.info("- Unmounting KDK") + logging.info("Unmounting KDK") result = subprocess.run(["hdiutil", "detach", mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if result.returncode != 0: - logging.info("- Failed to unmount KDK") + logging.info("Failed to unmount KDK") logging.info(result.stdout.decode("utf-8")) return - logging.info("- Removing KDK Disk Image") + logging.info("Removing KDK Disk Image") kdk_dmg_path.unlink() def _validate_installer_pkg(self, disk: str) -> bool: - verification_success = False + logging.info("Validating installer pkg") error_message = "" def _integrity_check(): nonlocal error_message - path = utilities.grab_mount_point_from_disk(disk + "s2") - dmg_path = path + f"/{path.split('/')[2]}.app/Contents/SharedSupport/SharedSupport.dmg" + for folder in Path(utilities.grab_mount_point_from_disk(disk + "s2")).glob("*.app"): + if folder.is_dir(): + dmg_path = folder / "Contents" / "SharedSupport" / "SharedSupport.dmg" + break + if not Path(dmg_path).exists(): logging.error(f"Failed to find {dmg_path}") error_message = f"Failed to find {dmg_path}" @@ -546,10 +560,10 @@ class macOSInstallerFlashFrame(wx.Frame): while thread.is_alive(): wx.Yield() - if verification_success: + if error_message == "": + logging.info("Installer pkg validated") return error_message - logging.error(error_message) return error_message diff --git a/resources/wx_gui/gui_main_menu.py b/resources/wx_gui/gui_main_menu.py index 5bd63500d..91b5502d7 100644 --- a/resources/wx_gui/gui_main_menu.py +++ b/resources/wx_gui/gui_main_menu.py @@ -8,10 +8,11 @@ import webbrowser from resources.wx_gui import ( gui_build, gui_macos_installer_download, - gui_sys_patch, gui_support, gui_help, gui_settings, + gui_sys_patch_start, + gui_sys_patch_display, gui_update, ) from resources import ( @@ -24,6 +25,7 @@ from data import os_data class MainFrame(wx.Frame): def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + logging.info("Initializing Main Menu Frame") super(MainFrame, self).__init__(parent, title=title, size=(600, 400), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) gui_support.GenerateMenubar(self, global_constants).generate() @@ -40,6 +42,7 @@ class MainFrame(wx.Frame): self.Centre() self.Show() + self._preflight_checks() @@ -213,11 +216,11 @@ class MainFrame(wx.Frame): pop_up.ShowModal() if pop_up.GetReturnCode() != wx.ID_YES: - print("- Skipping OpenCore and root volume patch update...") + print("Skipping OpenCore and root volume patch update...") return - print("- Updating OpenCore and root volume patches...") + print("Updating OpenCore and root volume patches...") self.constants.update_stage = gui_support.AutoUpdateStages.CHECKING self.Hide() pos = self.GetPosition() @@ -277,7 +280,7 @@ class MainFrame(wx.Frame): def on_post_install_root_patch(self, event: wx.Event = None): - gui_sys_patch.SysPatchFrame( + gui_sys_patch_display.SysPatchDisplayFrame( parent=self, title=self.title, global_constants=self.constants, diff --git a/resources/wx_gui/gui_settings.py b/resources/wx_gui/gui_settings.py index 97fd75167..21dd8a6e1 100644 --- a/resources/wx_gui/gui_settings.py +++ b/resources/wx_gui/gui_settings.py @@ -32,6 +32,7 @@ class SettingsFrame(wx.Frame): Modal-based Settings Frame """ def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + logging.info("Initializing Settings Frame") self.constants: constants.Constants = global_constants self.title: str = title self.parent: wx.Frame = parent @@ -823,6 +824,12 @@ class SettingsFrame(wx.Frame): "Check CHANGELOG before blindly updating.", ], }, + "Trigger Exception": { + "type": "button", + "function": self.on_test_exception, + "description": [ + ], + }, "wrap_around 1": { "type": "wrap_around", }, @@ -1257,11 +1264,16 @@ Hardware Information: def on_export_constants(self, event: wx.Event) -> None: # Throw pop up to get save location - with wx.FileDialog(self.parent, "Save Constants File", wildcard="JSON files (*.txt)|*.txt", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog: + with wx.FileDialog(self.parent, "Save Constants File", wildcard="JSON files (*.txt)|*.txt", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, defaultFile=f"constants-{self.constants.patcher_version}.txt") as fileDialog: if fileDialog.ShowModal() == wx.ID_CANCEL: return # Save the current contents in the file pathname = fileDialog.GetPath() + logging.info(f"Saving constants to {pathname}") with open(pathname, 'w') as file: - file.write(pprint.pformat(vars(self.constants), indent=4)) \ No newline at end of file + file.write(pprint.pformat(vars(self.constants), indent=4)) + + + def on_test_exception(self, event: wx.Event) -> None: + raise Exception("Test Exception") \ No newline at end of file diff --git a/resources/wx_gui/gui_support.py b/resources/wx_gui/gui_support.py index c97deb84a..9a9912cdb 100644 --- a/resources/wx_gui/gui_support.py +++ b/resources/wx_gui/gui_support.py @@ -3,10 +3,13 @@ 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 @@ -37,12 +40,15 @@ class GenerateMenubar: 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) @@ -50,8 +56,10 @@ class GenerateMenubar: class GaugePulseCallback: """ - Uses an alternative Pulse() method for wx.Gauge() on macOS Monterey+ + 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: @@ -66,6 +74,9 @@ class GaugePulseCallback: 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: @@ -106,6 +117,7 @@ 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 @@ -138,6 +150,7 @@ class CheckProperties: 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 @@ -149,6 +162,24 @@ class CheckProperties: 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: @@ -289,7 +320,7 @@ class RelaunchApplicationAsRoot: wx.Yield() - logging.info(f"- Relaunching as root with command: {program_arguments}") + logging.info(f"Relaunching as root with command: {program_arguments}") subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: diff --git a/resources/wx_gui/gui_sys_patch_display.py b/resources/wx_gui/gui_sys_patch_display.py new file mode 100644 index 000000000..6129a11dd --- /dev/null +++ b/resources/wx_gui/gui_sys_patch_display.py @@ -0,0 +1,338 @@ + +import wx +import os +import logging +import plistlib +import threading + +from pathlib import Path + +from resources import ( + constants, +) +from resources.sys_patch import ( + sys_patch_detect +) +from resources.wx_gui import ( + gui_main_menu, + gui_support, + gui_sys_patch_start, +) + + +class SysPatchDisplayFrame(wx.Frame): + """ + Create a modal frame for displaying root patches + """ + def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None): + logging.info("Initializing Root Patch Display Frame") + + if parent: + self.frame = parent + else: + super().__init__(parent, title=title, size=(360, 200), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX) + self.frame = self + self.frame.Centre() + + 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.init_with_parent = True if parent else False + + self.frame_modal = wx.Dialog(self.frame, title=title, size=(360, 200)) + + self._generate_elements_display_patches(self.frame_modal) + + if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE: + if self.available_patches is False: + gui_support.RestartHost(self.frame).restart(message="No root patch updates needed!\n\nWould you like to reboot to apply the new OpenCore build?") + + + def _generate_elements_display_patches(self, frame: wx.Frame = None) -> 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.Centre(wx.HORIZONTAL) + + # Label: Fetching patches... + available_label = wx.StaticText(frame, label="Fetching patches for host", 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.Centre(wx.HORIZONTAL) + + # Progress bar + progress_bar = wx.Gauge(frame, range=100, pos=(-1, available_label.GetPosition()[1] + available_label.GetSize()[1] + 10), size=(250, 20)) + progress_bar.Centre(wx.HORIZONTAL) + progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar) + progress_bar_animation.start_pulse() + + # Set window height + frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40)) + + # Labels: {patch name} + patches: dict = {} + def _fetch_patches(self) -> None: + nonlocal patches + patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() + + thread = threading.Thread(target=_fetch_patches, args=(self,)) + thread.start() + + frame.ShowWindowModal() + + while thread.is_alive(): + wx.Yield() + + + frame.Close() + + progress_bar.Hide() + progress_bar_animation.stop_pulse() + + available_label.SetLabel("Available patches for your system:") + available_label.Centre(wx.HORIZONTAL) + + + 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.Centre(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.Centre(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.Centre(wx.HORIZONTAL) + anchor.Hide() + + logging.info("Available patches:") + 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"- {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.Centre(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()[1] + 25)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) + patch_label.Centre(wx.HORIZONTAL) + + longest_patch = "" + for patch in patches: + if not patch.startswith("Validation"): + continue + if patches[patch] is False: + continue + if patch == "Validation: Unpatching Possible": + continue + + if len(patch) > len(longest_patch): + longest_patch = patch + anchor = wx.StaticText(frame, label=longest_patch.split('Validation: ')[1], pos=(-1, patch_label.GetPosition()[1] + 20)) + anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + anchor.Centre(wx.HORIZONTAL) + anchor.Hide() + + i = 0 + 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=(anchor.GetPosition()[0], anchor.GetPosition()[1] + i)) + patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + i = i + 20 + + if i == 20: + patch_label.SetLabel(patch_label.GetLabel().replace("-", "")) + patch_label.Centre(wx.HORIZONTAL) + + 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.Centre(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.Centre(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.on_start_root_patching(patches)) + start_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + start_button.Centre(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.on_revert_root_patching(patches)) + revert_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) + revert_button.Centre(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_dismiss if self.init_with_parent else 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.Centre(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() + else: + self.available_patches = False + if can_unpatch is False: + revert_button.Disable() + + # Relaunch as root if not root + if os.geteuid() != 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 + 15)) + frame.ShowWindowModal() + + + def on_start_root_patching(self, patches: dict): + frame = gui_sys_patch_start.SysPatchStartFrame( + parent=None, + title=self.title, + global_constants=self.constants, + patches=patches, + ) + frame.start_root_patching() + self.on_return_dismiss() if self.init_with_parent else self.on_return_to_main_menu() + + + def on_revert_root_patching(self, patches: dict): + frame = gui_sys_patch_start.SysPatchStartFrame( + parent=None, + title=self.title, + global_constants=self.constants, + patches=patches, + ) + frame.revert_root_patching() + self.on_return_dismiss() if self.init_with_parent else self.on_return_to_main_menu() + + + def on_return_to_main_menu(self, event: wx.Event = None): + # Get frame from event + frame_modal: wx.Dialog = event.GetEventObject().GetParent() + frame: wx.Frame = frame_modal.Parent + frame_modal.Hide() + frame.Hide() + + main_menu_frame = gui_main_menu.MainFrame( + None, + title=self.title, + global_constants=self.constants, + ) + main_menu_frame.Show() + frame.Destroy() + + + def on_return_dismiss(self, event: wx.Event = None): + self.frame_modal.Hide() + self.frame_modal.Destroy() + + + 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 + """ + + logging.info("Checking if new patches are needed") + + 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 isinstance(oclp_plist_data[key], (bool, int)): + continue + 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 \ No newline at end of file diff --git a/resources/wx_gui/gui_sys_patch.py b/resources/wx_gui/gui_sys_patch_start.py similarity index 54% rename from resources/wx_gui/gui_sys_patch.py rename to resources/wx_gui/gui_sys_patch_start.py index 69aab2d6f..6fe6483dc 100644 --- a/resources/wx_gui/gui_sys_patch.py +++ b/resources/wx_gui/gui_sys_patch_start.py @@ -1,6 +1,5 @@ import wx -import os import sys import time import logging @@ -14,6 +13,7 @@ from pathlib import Path from resources import ( constants, kdk_handler, + global_settings, ) from resources.sys_patch import ( sys_patch, @@ -27,38 +27,27 @@ from resources.wx_gui import ( from data import os_data -class SysPatchFrame(wx.Frame): +class SysPatchStartFrame(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 = {}): - self.frame = parent - self.initiated_with_parent = False - if not self.frame and patches == {}: - super(SysPatchFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) - self.frame = self - self.frame.Centre() - else: - self.initiated_with_parent = True + logging.info("Initializing Root Patching Frame") 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.patches: dict = patches - self.frame_modal = wx.Dialog(self.frame, title=title, size=(360, 200)) + super(SysPatchStartFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) + gui_support.GenerateMenubar(self, self.constants).generate() + 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.frame).restart(message="No root patch updates needed!\n\nWould you like to reboot to apply the new OpenCore build?") + if self.patches == {}: + self.patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set() def _kdk_download(self, frame: wx.Frame = None) -> bool: @@ -116,6 +105,7 @@ class SysPatchFrame(wx.Frame): if kdk_download_obj.download_complete is False: return False + logging.info("KDK download complete, validating with hdiutil") header.SetLabel(f"Validating KDK: {self.kdk_obj.kdk_url_build}") header.Centre(wx.HORIZONTAL) @@ -136,177 +126,13 @@ class SysPatchFrame(wx.Frame): progress_bar.SetValue(100) logging.info("KDK download complete") + + for child in frame.GetChildren(): + child.Destroy() + 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.Centre(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.Centre(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.Centre(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.Centre(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.Centre(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.Centre(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()[1] + 25)) - patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont")) - patch_label.Centre(wx.HORIZONTAL) - - longest_patch = "" - for patch in patches: - if not patch.startswith("Validation"): - continue - if patches[patch] is False: - continue - if patch == "Validation: Unpatching Possible": - continue - - if len(patch) > len(longest_patch): - longest_patch = patch - anchor = wx.StaticText(frame, label=longest_patch.split('Validation: ')[1], pos=(-1, patch_label.GetPosition()[1] + 20)) - anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) - anchor.Centre(wx.HORIZONTAL) - anchor.Hide() - - i = 0 - 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=(anchor.GetPosition()[0], anchor.GetPosition()[1] + i)) - patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) - i = i + 20 - - if i == 20: - patch_label.SetLabel(patch_label.GetLabel().replace("-", "")) - patch_label.Centre(wx.HORIZONTAL) - - 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.Centre(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.Centre(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(patches)) - start_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) - start_button.Centre(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(patches)) - revert_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont")) - revert_button.Centre(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_dismiss if self.initiated_with_parent else 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.Centre(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() - else: - self.available_patches = False - 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 @@ -346,9 +172,10 @@ class SysPatchFrame(wx.Frame): # Labels i = 0 + logging.info("Available patches:") 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]}") + logging.info(f"- {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 @@ -386,27 +213,20 @@ class SysPatchFrame(wx.Frame): dialog.ShowWindowModal() - def start_root_patching(self, patches: dict): - self.frame.Close() if self.frame else None - super(SysPatchFrame, self).__init__(None, title=self.title, size=(350, 260), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) - gui_support.GenerateMenubar(self, self.constants).generate() - self.Centre() - self.return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu) if self.return_button else None - + def start_root_patching(self): 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.patches["Settings: Kernel Debug Kit missing"] is True: if self._kdk_download(self) is False: - self.on_return_to_main_menu() - return + sys.exit(1) - self._generate_modal(patches, "Root Patching") + self._generate_modal(self.patches, "Root Patching") self.return_button.Disable() - thread = threading.Thread(target=self._start_root_patching, args=(patches,)) + thread = threading.Thread(target=self._start_root_patching, args=(self.patches,)) thread.start() while thread.is_alive(): @@ -422,23 +242,18 @@ class SysPatchFrame(wx.Frame): 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("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): - self.frame.Close() if self.frame else None - super(SysPatchFrame, self).__init__(None, title=self.title, size=(350, 260), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) - gui_support.GenerateMenubar(self, self.constants).generate() - self.Centre() - self.return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu) if self.return_button else None - + def revert_root_patching(self): logging.info("Reverting root patches") - self._generate_modal(patches, "Revert Root Patches") + + self._generate_modal(self.patches, "Revert Root Patches") self.return_button.Disable() - thread = threading.Thread(target=self._revert_root_patching, args=(patches,)) + thread = threading.Thread(target=self._revert_root_patching, args=(self.patches,)) thread.start() while thread.is_alive(): @@ -454,7 +269,7 @@ class SysPatchFrame(wx.Frame): 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("An internal error occurred while running the Root Patcher:\n") logging.error(traceback.format_exc()) logger.removeHandler(logger.handlers[2]) @@ -525,6 +340,8 @@ class SysPatchFrame(wx.Frame): Thus we'll need to see if the exact same OCLP build was used already """ + logging.info("Checking if new patches are needed") + if self.constants.commit_info[0] in ["Running from source", "Built from source"]: return True @@ -557,5 +374,5 @@ class SysPatchFrame(wx.Frame): logging.info(f"- Patch {patch} not installed") return True - logging.info("- No new patches detected for system") + logging.info("No new patches detected for system") return False \ No newline at end of file diff --git a/resources/wx_gui/gui_update.py b/resources/wx_gui/gui_update.py index 23746ffce..b3e862ef0 100644 --- a/resources/wx_gui/gui_update.py +++ b/resources/wx_gui/gui_update.py @@ -22,6 +22,7 @@ class UpdateFrame(wx.Frame): Create a frame for updating the patcher """ def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: wx.Point, url: str = "", version_label: str = "") -> None: + logging.info("Initializing Update Frame") if parent: self.parent: wx.Frame = parent @@ -55,6 +56,9 @@ class UpdateFrame(wx.Frame): self.version_label = version_label self.url = url + logging.info(f"Update URL: {url}") + logging.info(f"Update Version: {version_label}") + self.frame: wx.Frame = wx.Frame( parent=parent if parent else self, title=self.title, @@ -180,6 +184,7 @@ class UpdateFrame(wx.Frame): ["ditto", "-xk", str(self.constants.payload_path / "OpenCore-Patcher-GUI.app.zip"), str(self.constants.payload_path)], capture_output=True ) if result.returncode != 0: + logging.error(f"Failed to extract update. Error: {result.stderr.decode('utf-8')}") wx.CallAfter(self.progress_bar_animation.stop_pulse) 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) @@ -190,6 +195,7 @@ class UpdateFrame(wx.Frame): break if i == 1: + logging.error("Failed to extract update. Error: Update file does not exist") wx.CallAfter(self.progress_bar_animation.stop_pulse) wx.CallAfter(self.progress_bar.SetValue, 0) wx.CallAfter(wx.MessageBox, "Failed to extract update. Error: Update file does not exist", "Critical Error!", wx.OK | wx.ICON_ERROR) @@ -251,8 +257,10 @@ EOF wx.CallAfter(self.progress_bar_animation.stop_pulse) wx.CallAfter(self.progress_bar.SetValue, 0) if "User cancelled" in result.stderr.decode("utf-8"): + logging.info("User cancelled update") wx.CallAfter(wx.MessageBox, "User cancelled update", "Update Cancelled", wx.OK | wx.ICON_INFORMATION) else: + logging.critical(f"Failed to install update. Error: {result.stderr.decode('utf-8')}") 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)