Add support for KDK caching

This commit is contained in:
Mykola Grymalyuk
2023-11-01 21:55:29 -06:00
parent 4196a7b5f2
commit 55801e80bd
9 changed files with 306 additions and 26 deletions

View File

@@ -9,9 +9,19 @@
- Resolve Photos app crash
- Workaround tile window popup freezing apps by disabling the feature
- Workaround monochrome desktop widgets rendering issues by enforcing full color (can be disabled in OCLP settings)
- Add new Launch Daemon for clean up on macOS updates
- Resolves KDKless Macs failing to boot after updating from 14.0 to 14.x
- `/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist`
- Add new arguments:
- `--cache_os`: Cache nessasary patcher files for OS to be installed (ex. KDKs)
- `--prepare_for_update`: Clean up patcher files for OS to be installed (ex. /Library/Extensions)
- Add new Launch Daemons for handling macOS updates:
- `macos-update.plist`:
- Resolves KDKless Macs failing to boot after updating from 14.0 to 14.x
- Adds support for KDK caching for OS to be installed
- Invoked when update is staged
- `/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist`
- `os-caching.plist`
- Resolves unsupported/old KDKs from being used post-update
- Invoked when update is downloading
- `/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.os-caching.plist`
- Load UI icons from local path
- Resolves macOS downloader crash on slower machines
- Resolve iMac18,2 internal 4K display support

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AssociatedBundleIdentifiers</key>
<string>com.dortania.opencore-legacy-patcher</string>
<key>Label</key>
<string>com.dortania.opencore-legacy-patcher.rsr-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Application Support/Dortania/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher</string>
<string>--cache_os</string>
</array>
<key>WatchPaths</key>
<array>
<string>/System/Volumes/Update/Preflight.plist</string>
</array>
</dict>
</plist>

View File

@@ -11,6 +11,7 @@ from data import model_array, os_data
from resources.build import build
from resources.sys_patch import sys_patch, sys_patch_auto
from resources import defaults, utilities, validation, constants
from resources.wx_gui import gui_entry
# Generic building args
@@ -46,8 +47,11 @@ class arguments:
return
if self.args.prepare_for_update:
logging.info("Preparing host for macOS update")
self._clean_le_handler()
self._prepare_for_update_handler()
return
if self.args.cache_os:
self._cache_os_handler()
return
if self.args.auto_patch:
@@ -97,31 +101,47 @@ class arguments:
sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch()
def _prepare_for_update_handler(self) -> None:
"""
Prepare host for macOS update
"""
logging.info("Preparing host for macOS update")
os_data = utilities.fetch_staged_update(variant="Update")
if os_data[0] is None:
logging.info("No update staged, skipping")
return
os_version = os_data[0]
os_build = os_data[1]
logging.info(f"Preparing for update to {os_version} ({os_build})")
self._clean_le_handler()
def _cache_os_handler(self) -> None:
"""
Fetch KDK for incoming OS
"""
results = subprocess.run(["ps", "-ax"], stdout=subprocess.PIPE)
if results.stdout.decode("utf-8").count("OpenCore-Patcher --cache_os") > 1:
logging.info("Another instance of OS caching is running, exiting")
return
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.OS_CACHE)
def _clean_le_handler(self) -> None:
"""
Check if software update is staged
If so, clean /Library/Extensions
Clean /Library/Extensions of problematic kexts
Note macOS Ventura and older do this automatically
"""
if self.constants.detected_os < os_data.os_data.sonoma:
logging.info("Host doesn't require cleaning, skipping")
return
update_config = "/System/Volumes/Update/Update.plist"
if not Path(update_config).exists():
logging.info("No update staged, skipping")
return
try:
update_staged = plistlib.load(open(update_config, "rb"))
except Exception as e:
logging.error(f"Failed to load update config: {e}")
return
if "update-asset-attributes" not in update_staged:
logging.info("No update staged, skipping")
return
logging.info("Update staged, cleaning /Library/Extensions")
logging.info("Cleaning /Library/Extensions")
for kext in Path("/Library/Extensions").glob("*.kext"):
if not Path(f"{kext}/Contents/Info.plist").exists():

View File

@@ -296,6 +296,10 @@ class Constants:
def update_launch_daemon_path(self):
return self.launch_services_path / Path("com.dortania.opencore-legacy-patcher.macos-update.plist")
@property
def kdk_launch_daemon_path(self):
return self.launch_services_path / Path("com.dortania.opencore-legacy-patcher.os-caching.plist")
# ACPI
@property
def pci_ssdt_path(self):

View File

@@ -648,7 +648,10 @@ class PatchSysVolume:
self._execute_patchset(sys_patch_generate.GenerateRootPatchSets(self.computer.real_model, self.constants, self.hardware_details).patchset)
if self.constants.wxpython_variant is True and self.constants.detected_os >= os_data.os_data.big_sur:
sys_patch_auto.AutomaticSysPatch(self.constants).install_auto_patcher_launch_agent()
needs_daemon = False
if self.constants.detected_os >= os_data.os_data.ventura and self.skip_root_kmutil_requirement is False:
needs_daemon = True
sys_patch_auto.AutomaticSysPatch(self.constants).install_auto_patcher_launch_agent(kdk_caching_needed=needs_daemon)
self._rebuild_root_volume()

View File

@@ -9,6 +9,7 @@ import logging
import plistlib
import subprocess
import webbrowser
import hashlib
from pathlib import Path
@@ -331,7 +332,7 @@ Please check the Github page for more information about this release."""
logging.info("- Unable to determine if boot disk is removable, skipping prompt")
def install_auto_patcher_launch_agent(self):
def install_auto_patcher_launch_agent(self, kdk_caching_needed: bool = False):
"""
Install the Auto Patcher Launch Agent
@@ -350,12 +351,16 @@ Please check the Github page for more information about this release."""
self.constants.auto_patch_launch_agent_path: "/Library/LaunchAgents/com.dortania.opencore-legacy-patcher.auto-patch.plist",
self.constants.update_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist",
**({ self.constants.rsr_monitor_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.rsr-monitor.plist" } if self._create_rsr_monitor_daemon() else {}),
**({ self.constants.kdk_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.os-caching.plist" } if kdk_caching_needed is True else {} ),
}
for service in services:
name = Path(service).name
logging.info(f"- Installing {name}")
if Path(services[service]).exists():
if hashlib.sha256(open(service, "rb").read()).hexdigest() == hashlib.sha256(open(services[service], "rb").read()).hexdigest():
logging.info(f" - {name} checksums match, skipping")
continue
logging.info(f" - Existing service found, removing")
utilities.process_status(utilities.elevated(["rm", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
# Create parent directories

View File

@@ -553,6 +553,33 @@ def elevated(*args, **kwargs) -> subprocess.CompletedProcess:
return subprocess.run(["sudo"] + [args[0][0]] + args[0][1:], **kwargs)
def fetch_staged_update(variant: str = "Update") -> (str, str):
"""
Check for staged macOS update
Supported variants:
- Preflight
- Update
"""
os_build = None
os_version = None
update_config = f"/System/Volumes/Update/{variant}.plist"
if not Path(update_config).exists():
return (None, None)
try:
update_staged = plistlib.load(open(update_config, "rb"))
except:
return (None, None)
if "update-asset-attributes" not in update_staged:
return (None, None)
os_build = update_staged["update-asset-attributes"]["Build"]
os_version = update_staged["update-asset-attributes"]["OSVersion"]
return os_version, os_build
def check_cli_args():
parser = argparse.ArgumentParser()
parser.add_argument("--build", help="Build OpenCore", action="store_true", required=False)
@@ -581,6 +608,7 @@ def check_cli_args():
parser.add_argument("--patch_sys_vol", help="Patches root volume", action="store_true", required=False)
parser.add_argument("--unpatch_sys_vol", help="Unpatches root volume, EXPERIMENTAL", action="store_true", required=False)
parser.add_argument("--prepare_for_update", help="Prepares host for macOS update, ex. clean /Library/Extensions", action="store_true", required=False)
parser.add_argument("--cache_os", help="Caches patcher files (ex. KDKs) for incoming OS in Preflight.plist", action="store_true", required=False)
# validation args
parser.add_argument("--validate", help="Runs Validation Tests for CI", action="store_true", required=False)
@@ -598,7 +626,8 @@ def check_cli_args():
args.unpatch_sys_vol or
args.validate or
args.auto_patch or
args.prepare_for_update
args.prepare_for_update or
args.cache_os
):
return None
else:

View File

@@ -0,0 +1,188 @@
"""
UI to display to users before a macOS update is applied
Primarily for caching updates required for incoming OS (ex. KDKs)
"""
import wx
import sys
import time
import logging
import threading
from pathlib import Path
from resources import constants, kdk_handler, utilities
from resources.wx_gui import gui_support, gui_download
class OSUpdateFrame(wx.Frame):
"""
Create a modal frame for displaying information to the user before an update is applied
"""
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
logging.info("Initializing Prepare Update Frame")
if parent:
self.frame = parent
else:
super().__init__(parent, title=title, size=(360, 140), 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
os_data = utilities.fetch_staged_update(variant="Preflight")
if os_data[0] is None:
logging.info("No staged update found")
self._exit()
logging.info(f"Staged update found: {os_data[0]} ({os_data[1]})")
self.os_data = os_data
self._generate_ui()
self.kdk_obj: kdk_handler.KernelDebugKitObject = None
def _kdk_thread_spawn():
self.kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.os_data[1], self.os_data[0], passive=True)
kdk_thread = threading.Thread(target=_kdk_thread_spawn)
kdk_thread.start()
while kdk_thread.is_alive():
wx.Yield()
if self.kdk_obj.success is False:
self._exit()
kdk_download_obj = self.kdk_obj.retrieve_download()
if not kdk_download_obj:
# KDK is already downloaded
# Return false since we didn't display anything
self._exit()
self.kdk_download_obj = kdk_download_obj
self.frame.Show()
self.did_cancel = False
self._notifyUser()
# Allow 10 seconds for the user to cancel the download
# If nothing, continue
for i in range(0, 10):
if self.did_cancel is True:
self._exit()
time.sleep(1)
gui_download.DownloadFrame(
self,
title=self.title,
global_constants=self.constants,
download_obj=kdk_download_obj,
item_name=f"KDK Build {self.kdk_obj.kdk_url_build}"
)
if kdk_download_obj.download_complete is False:
self._exit()
logging.info("KDK download complete, validating with hdiutil")
self.kdk_checksum_result = False
def _validate_kdk_checksum_thread():
self.kdk_checksum_result = self.kdk_obj.validate_kdk_checksum()
kdk_checksum_thread = threading.Thread(target=_validate_kdk_checksum_thread)
kdk_checksum_thread.start()
while kdk_checksum_thread.is_alive():
wx.Yield()
if self.kdk_checksum_result is False:
logging.error("KDK checksum validation failed")
logging.error(self.kdk_obj.error_msg)
self._exit()
logging.info("KDK checksum validation passed")
logging.info("Mounting KDK")
if not Path(self.constants.kdk_download_path).exists():
logging.error("KDK download path does not exist")
self._exit()
self.kdk_install_result = False
def _install_kdk_thread():
self.kdk_install_result = kdk_handler.KernelDebugKitUtilities().install_kdk_dmg(self.constants.kdk_download_path, only_install_backup=True)
kdk_install_thread = threading.Thread(target=_install_kdk_thread)
kdk_install_thread.start()
while kdk_install_thread.is_alive():
wx.Yield()
if self.kdk_install_result is False:
logging.info("Failed to install KDK")
self._exit()
logging.info("KDK installed successfully")
self._exit()
def _generate_ui(self) -> None:
"""
Display frame
Title: OpenCore Legacy Patcher is preparing to update your system
Body: Please wait while we prepare your system for the update.
This may take a few minutes.
"""
header = wx.StaticText(self.frame, label="Preparing for macOS Software Update", pos=(-1,5))
header.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
header.Centre(wx.HORIZONTAL)
# list OS
label = wx.StaticText(self.frame, label=f"macOS {self.os_data[0]} ({self.os_data[1]})", pos=(-1, 35))
label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
label.Centre(wx.HORIZONTAL)
# this may take a few minutes
label = wx.StaticText(self.frame, label="This may take a few minutes.", pos=(-1, 55))
label.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
label.Centre(wx.HORIZONTAL)
# Add a progress bar
self.progress_bar = wx.Gauge(self.frame, range=100, pos=(10, 75), size=(340, 20))
self.progress_bar.SetValue(0)
self.progress_bar.Pulse()
# Set frame size below progress bar
self.frame.SetSize((360, 140))
def _notifyUser(self) -> None:
"""
Notify user of what OCLP is doing
Note will be spawned through wx.CallAfter
"""
threading.Thread(target=self._notifyUserThread).start()
def _notifyUserThread(self) -> None:
"""
Notify user of what OCLP is doing
"""
message=f"OpenCore Legacy Patcher has detected that a macOS update is being downloaded:\n{self.os_data[0]} ({self.os_data[1]})\n\nThe patcher needs to prepare the system for the update, and will download any additional resources it may need post-update.\n\nThis may take a few minutes, the patcher will exit when it is done."
# Yes/No for caching
dlg = wx.MessageDialog(self.frame, message=message, caption="OpenCore Legacy Patcher", style=wx.YES_NO | wx.ICON_INFORMATION)
dlg.SetYesNoLabels("&Ok", "&Cancel")
result = dlg.ShowModal()
if result == wx.ID_NO:
logging.info("User cancelled OS caching")
self.kdk_download_obj.stop()
self.did_cancel = True
def _exit(self):
"""
Exit the frame
"""
self.frame.Close()
sys.exit()

View File

@@ -6,6 +6,7 @@ import logging
from resources import constants
from resources.wx_gui import (
gui_cache_os_update,
gui_main_menu,
gui_build,
gui_install_oc,
@@ -24,6 +25,7 @@ class SupportedEntryPoints:
INSTALL_OC = gui_install_oc.InstallOCFrame
SYS_PATCH = gui_sys_patch_start.SysPatchStartFrame
UPDATE_APP = gui_update.UpdateFrame
OS_CACHE = gui_cache_os_update.OSUpdateFrame
class EntryPoint: