Add AMFIPass (#1051)

This commit is contained in:
Dhinak G
2023-07-26 08:29:46 -04:00
committed by GitHub
parent aa1739c1d6
commit 43be00e9e7
17 changed files with 247 additions and 138 deletions

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ __pycache__/
/payloads/OpenCore-Legacy-Patcher
/payloads/InstallAssistant.pkg.integrityDataV1
/payloads.dmg
/Universal-Binaries.dmg
/payloads/OpenCore-Legacy-Patcher-*.plist
/payloads/KDK.dmg
*.log

View File

@@ -20,7 +20,7 @@ class CreateBinary:
Library for creating OpenCore-Patcher application
This script's main purpose is to handle the following:
- Download external dependancies (ex. PatcherSupportPkg)
- Download external dependencies (ex. PatcherSupportPkg)
- Convert payloads directory into DMG
- Build Binary via Pyinstaller
- Patch 'LC_VERSION_MIN_MACOSX' to OS X 10.10
@@ -275,7 +275,10 @@ class CreateBinary:
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}")
# Just to be safe
assert resource, "Resource cannot be empty"
assert resource not in ("/", "."), "Resource cannot be root"
rm_output = subprocess.run(
["rm", "-rf", f"./{resource}"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE

View File

@@ -18,6 +18,7 @@ This patcher is made of multiple external applications from different people and
* [telemetrap](https://forums.macrumors.com/threads/mp3-1-others-sse-4-2-emulation-to-enable-amd-metal-driver.2206682/post-28447707) - Syncretic
* [SurPlus](https://github.com/reenigneorcim/SurPlus) - Syncretic
* [VMM Patch Set](https://github.com/dortania/OpenCore-Legacy-Patcher/blob/4a8f61a01da72b38a4b2250386cc4b497a31a839/payloads/Config/config.plist#L1222-L1281) - parrotgeek1
* AMFIPass - Dhinak G
* Apple Binaries - Apple Inc.
* All other patches - respective authors

View File

@@ -1584,6 +1584,22 @@
<dict>
<key>Arch</key>
<string>x86_64</string>
<key>BundlePath</key>
<string>AMFIPass.kext</string>
<key>Comment</key>
<string>AMFIPass</string>
<key>Enabled</key>
<false/>
<key>ExecutablePath</key>
<string>Contents/MacOS/AMFIPass</string>
<key>MaxKernel</key>
<string></string>
<key>MinKernel</key>
<string>20.0.0</string>
<key>PlistPath</key>
<string>Contents/Info.plist</string>
</dict>
<dict>
<key>Comment</key>
<string>Aquantia Ethernet Patch</string>
<key>Enabled</key>

Binary file not shown.

View File

@@ -82,3 +82,6 @@ class BuildSecurity:
support.BuildSupport(self.model, self.constants, self.config).get_item_by_kv(self.config["Kernel"]["Patch"], "Comment", "Reroute kern.hv_vmm_present patch (1)")["Enabled"] = True
support.BuildSupport(self.model, self.constants, self.config).get_item_by_kv(self.config["Kernel"]["Patch"], "Comment", "Reroute kern.hv_vmm_present patch (2) Legacy")["Enabled"] = True
support.BuildSupport(self.model, self.constants, self.config).get_item_by_kv(self.config["Kernel"]["Patch"], "Comment", "Reroute kern.hv_vmm_present patch (2) Ventura")["Enabled"] = True
logging.info("- Enabling AMFIPass")
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AMFIPass.kext", self.constants.amfipass_version, self.constants.amfipass_path)

View File

@@ -4,6 +4,7 @@
from pathlib import Path
from typing import Optional
from packaging import version
from resources import device_probe
from data import os_data
@@ -78,13 +79,15 @@ class Constants:
## Dortania
## https://github.com/dortania
self.backlight_injector_version: str = "1.1.0" # BacklightInjector
self.backlight_injectorA_version: str = "1.0.0" # BacklightInjector (iMac9,1)
self.smcspoof_version: str = "1.0.0" # SMC-Spoof
self.mce_version: str = "1.0.0" # AppleMCEReporterDisabler
self.btspoof_version: str = "1.0.0" # Bluetooth-Spoof
self.aspp_override_version: str = "1.0.1" # ACPI_SMC_PlatformPlugin Override
self.rsrhelper_version: str = "1.0.0" # RSRHelper
self.backlight_injector_version: str = "1.1.0" # BacklightInjector
self.backlight_injectorA_version: str = "1.0.0" # BacklightInjector (iMac9,1)
self.smcspoof_version: str = "1.0.0" # SMC-Spoof
self.mce_version: str = "1.0.0" # AppleMCEReporterDisabler
self.btspoof_version: str = "1.0.0" # Bluetooth-Spoof
self.aspp_override_version: str = "1.0.1" # ACPI_SMC_PlatformPlugin Override
self.rsrhelper_version: str = "1.0.0" # RSRHelper
self.amfipass_version: str = "1.3.1" # AMFIPass
self.amfipass_compatibility_version: str = "1.2.1" # Minimum AMFIPass version required
## Syncretic
## https://forums.macrumors.com/members/syncretic.1173816/
@@ -232,6 +235,18 @@ class Constants:
os_data.os_data.ventura,
]
@property
def special_build(self):
"""
Special builds are used for testing. They do not get updates through the updater
"""
try:
version.parse(self.patcher_version)
return False
except version.InvalidVersion:
return True
# Payload Location
# Support Disk Images
@@ -489,6 +504,11 @@ class Constants:
def rsrhelper_path(self):
return self.payload_kexts_path / Path(f"Acidanthera/RSRHelper-v{self.rsrhelper_version}-{self.kext_variant}.zip")
@property
def amfipass_path(self):
# AMFIPass is release only
return self.payload_kexts_path / Path(f"Acidanthera/AMFIPass-v{self.amfipass_version}-RELEASE.zip")
@property
def innie_path(self):
return self.payload_kexts_path / Path(f"Misc/Innie-v{self.innie_version}-{self.kext_variant}.zip")

View File

@@ -49,7 +49,7 @@ class GenerateDefaults:
self._networking_probe()
self._misc_hardwares_probe()
self._smbios_probe()
self._check_amfipass_supported()
def _general_probe(self) -> None:
"""
@@ -309,4 +309,32 @@ class GenerateDefaults:
if is_key_enabled not in ["false", "0"]:
subprocess.run(["defaults", "write", "-g", key, "-bool", "true"])
subprocess.run(["defaults", "write", "-g", "Amy.MenuBar2Beta", "-bool", "false"])
subprocess.run(["defaults", "write", "-g", "Amy.MenuBar2Beta", "-bool", "false"])
def _check_amfipass_supported(self) -> None:
"""
Check if root volume supports AMFIPass
The basic requirements of this function are:
- The host is the target
- Root volume doesn't have adhoc signed binaries
If all of these conditions are met, it is safe to disable AMFI and CS_LV. Otherwise, for safety, leave it be.
"""
if not self.host_is_target:
# Unknown whether the host is using old binaries
# Rebuild it once you are on the host
return
# Check for adhoc signed binaries
if self.constants.computer.oclp_sys_signed is False:
# Root patch with new binaries, then reboot
return
# Note: simply checking the authority is not enough, as the authority can be spoofed
# (but do we really care? this is just a simple check)
# Note: the cert will change
self.constants.disable_amfi = False
self.constants.disable_cs_lv = False

View File

@@ -604,6 +604,7 @@ class Computer:
oclp_sys_version: Optional[str] = None
oclp_sys_date: Optional[str] = None
oclp_sys_url: Optional[str] = None
oclp_sys_signed: Optional[bool] = False
firmware_vendor: Optional[str] = None
rosetta_active: Optional[bool] = False
@@ -927,15 +928,19 @@ class Computer:
def oclp_sys_patch_probe(self):
path = Path("/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist")
if path.exists():
sys_plist = plistlib.load(path.open("rb"))
if sys_plist:
if "OpenCore Legacy Patcher" in sys_plist:
self.oclp_sys_version = sys_plist["OpenCore Legacy Patcher"]
if "Time Patched" in sys_plist:
self.oclp_sys_date = sys_plist["Time Patched"]
if "Commit URL" in sys_plist:
self.oclp_sys_url = sys_plist["Commit URL"]
if not path.exists():
self.oclp_sys_signed = True # No plist, so assume root is valid
return
sys_plist = plistlib.load(path.open("rb"))
if sys_plist:
if "OpenCore Legacy Patcher" in sys_plist:
self.oclp_sys_version = sys_plist["OpenCore Legacy Patcher"]
if "Time Patched" in sys_plist:
self.oclp_sys_date = sys_plist["Time Patched"]
if "Commit URL" in sys_plist:
self.oclp_sys_url = sys_plist["Commit URL"]
if "Custom Signature" in sys_plist:
self.oclp_sys_signed = sys_plist["Custom Signature"]
def check_rosetta(self):
result = subprocess.run("sysctl -in sysctl.proc_translated".split(), stdout=subprocess.PIPE).stdout.decode()

View File

@@ -20,7 +20,7 @@ from data import os_data
KDK_INSTALL_PATH: str = "/Library/Developer/KDKs"
KDK_INFO_PLIST: str = "KDKInfo.plist"
KDK_API_LINK: str = "https://raw.githubusercontent.com/dortania/KdkSupportPkg/gh-pages/manifest.json"
KDK_API_LINK: str = "https://dortania.github.io/KdkSupportPkg/manifest.json"
KDK_ASSET_LIST: list = None

View File

@@ -67,7 +67,6 @@ class PatchSysVolume:
self.skip_root_kmutil_requirement = self.hardware_details["Settings: Supports Auxiliary Cache"]
def _init_pathing(self, custom_root_mount_path: Path = None, custom_data_mount_path: Path = None) -> None:
"""
Initializes the pathing for root volume patching
@@ -493,7 +492,7 @@ class PatchSysVolume:
oclp_plist_data = plistlib.load(Path(oclp_path).open("rb"))
for key in oclp_plist_data:
if isinstance(oclp_plist_data[key], (bool, int)):
continue
continue
if "Install" not in oclp_plist_data[key]:
continue
for location in oclp_plist_data[key]["Install"]:

View File

@@ -47,8 +47,7 @@ class AutomaticSysPatch:
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
if dict:
for key in dict:
version = dict[key]["Version"]
version = dict["Version"]
logging.info(f"- Found new version: {version}")
app = wx.App()
@@ -64,7 +63,7 @@ class AutomaticSysPatch:
if response == wx.ID_YES:
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.UPDATE_APP)
elif response == wx.ID_NO:
webbrowser.open(dict[key]["Github Link"])
webbrowser.open(dict["Github Link"])
return
if utilities.check_seal() is True:
@@ -149,17 +148,21 @@ class AutomaticSysPatch:
logging.info("- Versions match")
return True
if self.constants.special_build is True:
# Version doesn't match and we're on a special build
# Special builds don't have good ways to compare versions
logging.info("- Special build detected, assuming installed is older")
return False
# Check if installed version is newer than booted version
if updates.CheckBinaryUpdates(self.constants)._check_if_build_newer(
self.constants.computer.oclp_version.split("."), self.constants.patcher_version.split(".")
) is True:
if updates.CheckBinaryUpdates(self.constants).check_if_newer(self.constants.computer.oclp_version):
logging.info("- Installed version is newer than booted version")
return True
args = [
"osascript",
"-e",
f"""display dialog "OpenCore Legacy Patcher has detected that you are booting an outdated OpenCore build\n- Booted: {self.constants.computer.oclp_version}\n- Installed: {self.constants.patcher_version}\n\nWould you like to update the OpenCore bootloader?" """
f"""display dialog "OpenCore Legacy Patcher has detected that you are booting {'a different' if self.constants.special_build else 'an outdated'} OpenCore build\n- Booted: {self.constants.computer.oclp_version}\n- Installed: {self.constants.patcher_version}\n\nWould you like to update the OpenCore bootloader?" """
f'with icon POSIX file "{self.constants.app_icon_path}"',
]
output = subprocess.run(

View File

@@ -3,26 +3,16 @@
# Used when supplying data to sys_patch.py
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
import plistlib
import logging
import py_sip_xnu
import plistlib
from pathlib import Path
from resources import (
constants,
device_probe,
utilities,
amfi_detect,
network_handler,
kdk_handler
)
from data import (
model_array,
os_data,
sip_data,
smbios_data,
cpu_data
)
import packaging.version
import py_sip_xnu
from data import cpu_data, model_array, os_data, sip_data, smbios_data
from resources import (amfi_detect, constants, device_probe, kdk_handler,
network_handler, utilities)
class DetectRootPatch:
@@ -418,7 +408,7 @@ class DetectRootPatch:
bool: True if loaded, False otherwise
"""
return utilities.check_kext_loaded("WhateverGreen", self.constants.detected_os)
return utilities.check_kext_loaded("as.vit9696.WhateverGreen")
def _check_kdk(self):
@@ -521,7 +511,7 @@ class DetectRootPatch:
if self.constants.detected_os > os_data.os_data.catalina:
self.brightness_legacy = True
if self.model in ["iMac7,1", "iMac8,1"] or (self.model in model_array.LegacyAudio and utilities.check_kext_loaded("AppleALC", self.constants.detected_os) is False):
if self.model in ["iMac7,1", "iMac8,1"] or (self.model in model_array.LegacyAudio and utilities.check_kext_loaded("as.vit9696.AppleALC") is False):
# Special hack for systems with botched GOPs
# TL;DR: No Boot Screen breaks Lilu, therefore breaking audio
if self.constants.detected_os > os_data.os_data.catalina:
@@ -610,6 +600,12 @@ class DetectRootPatch:
if self.constants.detected_os < os_data.os_data.big_sur:
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
amfipass_version = utilities.check_kext_loaded("com.dhinakg.AMFIPass")
if amfipass_version:
if packaging.version.parse(amfipass_version) >= packaging.version.parse(self.constants.amfipass_compatibility_version):
# If AMFIPass is loaded, our binaries will work
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
if self.constants.detected_os >= os_data.os_data.ventura:
if self.amfi_shim_bins is True:
# Currently we require AMFI outright disabled

View File

@@ -3,8 +3,11 @@
# Call check_binary_updates() to determine if any updates are available
# Returns dict with Link and Version of the latest binary update if available
import logging
from typing import Optional, Union
from resources import network_handler, constants
from packaging import version
from resources import constants, network_handler
REPO_LATEST_RELEASE_URL: str = "https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest"
@@ -12,48 +15,62 @@ REPO_LATEST_RELEASE_URL: str = "https://api.github.com/repos/dortania/OpenCore-L
class CheckBinaryUpdates:
def __init__(self, global_constants: constants.Constants) -> None:
self.constants: constants.Constants = global_constants
try:
self.binary_version = version.parse(self.constants.patcher_version)
except version.InvalidVersion:
assert self.constants.special_build is True, "Invalid version number for binary"
# Special builds will not have a proper version number
self.binary_version = version.parse("0.0.0")
self.binary_version = self.constants.patcher_version
self.binary_version_array = [int(x) for x in self.binary_version.split(".")]
self.latest_details = None
def _check_if_build_newer(self, remote_version: list = None, local_version: list = None) -> bool:
def check_if_newer(self, version: Union[str, version.Version]) -> bool:
"""
Check if the remote version is newer than the local version
Check if the provided version is newer than the local version
Parameters:
remote_version (list): Remote version to compare against
local_version (list): Local version to compare against
version (str): Version to compare against
Returns:
bool: True if remote version is newer, False if not
bool: True if the provided version is newer, False if not
"""
if self.constants.special_build is True:
return False
return self._check_if_build_newer(version, self.binary_version)
def _check_if_build_newer(self, first_version: Union[str, version.Version], second_version: Union[str, version.Version]) -> bool:
"""
Check if the first version is newer than the second version
Parameters:
first_version_str (str): First version to compare against (generally local)
second_version_str (str): Second version to compare against (generally remote)
Returns:
bool: True if first version is newer, False if not
"""
if remote_version is None:
remote_version = self.remote_version_array
if local_version is None:
local_version = self.binary_version_array
if not isinstance(first_version, version.Version):
try:
first_version = version.parse(first_version)
except version.InvalidVersion:
# Special build > release build: assume special build is newer
return True
if not isinstance(second_version, version.Version):
try:
second_version = version.parse(second_version)
except version.InvalidVersion:
# Release build > special build: assume special build is newer
return False
if local_version == remote_version:
if first_version == second_version:
if not self.constants.commit_info[0].startswith("refs/tags"):
# Check for nightly builds
return True
# Pad version numbers to match length (ie. 0.1.0 vs 0.1.0.1)
while len(remote_version) > len(local_version):
local_version.append(0)
while len(remote_version) < len(local_version):
remote_version.append(0)
for i in range(0, len(remote_version)):
if int(remote_version[i]) < int(local_version[i]):
break
elif int(remote_version[i]) > int(local_version[i]):
return True
return False
return first_version > second_version
def _determine_local_build_type(self) -> str:
"""
@@ -63,11 +80,7 @@ class CheckBinaryUpdates:
str: "GUI" or "TUI"
"""
if self.constants.wxpython_variant is True:
return "GUI"
else:
return "TUI"
return "GUI" if self.constants.wxpython_variant else "TUI"
def _determine_remote_type(self, remote_name: str) -> str:
"""
@@ -87,8 +100,7 @@ class CheckBinaryUpdates:
else:
return "Unknown"
def check_binary_updates(self) -> dict:
def check_binary_updates(self) -> Optional[dict]:
"""
Check if any updates are available for the OpenCore Legacy Patcher binary
@@ -96,7 +108,13 @@ class CheckBinaryUpdates:
dict: Dictionary with Link and Version of the latest binary update if available
"""
available_binaries: list = {}
if self.constants.special_build is True:
# Special builds do not get updates through the updater
return None
if self.latest_details:
# We already checked
return self.latest_details
if not network_handler.NetworkUtilities(REPO_LATEST_RELEASE_URL).verify_network_connection():
return None
@@ -107,26 +125,26 @@ class CheckBinaryUpdates:
if "tag_name" not in data_set:
return None
self.remote_version = data_set["tag_name"]
# The release marked as latest will always be stable, and thus, have a proper version number
# But if not, let's not crash the program
try:
latest_remote_version = version.parse(data_set["tag_name"])
except version.InvalidVersion:
return None
self.remote_version_array = self.remote_version.split(".")
self.remote_version_array = [int(x) for x in self.remote_version_array]
if self._check_if_build_newer() is False:
if not self._check_if_build_newer(latest_remote_version, self.binary_version):
return None
for asset in data_set["assets"]:
logging.info(f"Found asset: {asset['name']}")
if self._determine_remote_type(asset["name"]) == self._determine_local_build_type():
available_binaries.update({
asset['name']: {
"Name": asset["name"],
"Version": self.remote_version,
"Link": asset["browser_download_url"],
"Type": self._determine_remote_type(asset["name"]),
"Github Link": f"https://github.com/dortania/OpenCore-Legacy-Patcher/releases/{self.remote_version}"
}
})
return available_binaries
self.latest_details = {
"Name": asset["name"],
"Version": latest_remote_version,
"Link": asset["browser_download_url"],
"Type": self._determine_remote_type(asset["name"]),
"Github Link": f"https://github.com/dortania/OpenCore-Legacy-Patcher/releases/{latest_remote_version}",
}
return self.latest_details
return None
return None

View File

@@ -1,21 +1,20 @@
# Copyright (C) 2020-2023, Dhinak G, Mykola Grymalyuk
import argparse
import atexit
import binascii
import logging
import math
import os
import plistlib
import re
import shutil
import subprocess
from pathlib import Path
import os
import binascii
import argparse
import atexit
import shutil
import py_sip_xnu
import logging
from data import os_data, sip_data
from resources import constants, ioreg
from data import sip_data, os_data
def hexswap(input_hex: str):
@@ -173,15 +172,35 @@ def enable_sleep_after_running():
sleep_process = None
def check_kext_loaded(kext_name, os_version):
if os_version > os_data.os_data.catalina:
kext_loaded = subprocess.run(["kmutil", "showloaded", "--list-only", "--variant-suffix", "release"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
else:
kext_loaded = subprocess.run(["kextstat", "-l"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if kext_name in kext_loaded.stdout.decode():
return True
else:
return False
def check_kext_loaded(bundle_id: str) -> str:
"""
Checks if a kext is loaded
Parameters:
bundle_id (str): The bundle ID of the kext to check
Returns:
str: The version of the kext if it is loaded, or "" if it is not loaded
"""
# Name (Version) UUID <Linked Against>
# no UUID for kextstat
pattern = re.compile(re.escape(bundle_id) + r"\s+\((?P<version>.+)\)")
args = ["kextstat", "-l", "-b", bundle_id]
if Path("/usr/bin/kmutil").exists():
args = ["kmutil", "showloaded", "--list-only", "--variant-suffix", "release", "--optional-identifier", bundle_id]
kext_loaded = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if kext_loaded.returncode != 0:
return ""
output = kext_loaded.stdout.decode()
if not output.strip():
return ""
match = pattern.search(output)
if match:
return match.group("version")
return ""
def check_oclp_boot():

View File

@@ -65,7 +65,7 @@ class MainFrame(wx.Frame):
"""
# Title label: OpenCore Legacy Patcher v{X.Y.Z}
title_label = wx.StaticText(self, label=f"OpenCore Legacy Patcher v{self.constants.patcher_version}", pos=(-1,10))
title_label = wx.StaticText(self, label=f"OpenCore Legacy Patcher {'' if self.constants.special_build else 'v'}{self.constants.patcher_version}", 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)
@@ -282,22 +282,21 @@ class MainFrame(wx.Frame):
if not dict:
return
for entry in dict:
version = dict[entry]["Version"]
logging.info(f"New version: {version}")
dialog = wx.MessageDialog(
parent=self,
message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?",
caption="Update Available for OpenCore Legacy Patcher!",
style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
)
dialog.SetYesNoCancelLabels("Download and install", "Ignore", "View on Github")
response = dialog.ShowModal()
version = dict["Version"]
logging.info(f"New version: {version}")
dialog = wx.MessageDialog(
parent=self,
message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?",
caption="Update Available for OpenCore Legacy Patcher!",
style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
)
dialog.SetYesNoCancelLabels("Download and install", "Ignore", "View on Github")
response = dialog.ShowModal()
if response == wx.ID_YES:
wx.CallAfter(self.on_update, dict[entry]["Link"], version)
elif response == wx.ID_CANCEL:
webbrowser.open(dict[entry]["Github Link"])
if response == wx.ID_YES:
wx.CallAfter(self.on_update, dict["Link"], version)
elif response == wx.ID_CANCEL:
webbrowser.open(dict["Github Link"])
def on_build_and_install(self, event: wx.Event = None):

View File

@@ -48,10 +48,8 @@ class UpdateFrame(wx.Frame):
if url == "" or version_label == "":
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
if dict:
for key in dict:
version_label = dict[key]["Version"]
url = dict[key]["Link"]
break
version_label = dict["Version"]
url = dict["Link"]
else:
wx.MessageBox("Failed to get update info", "Critical Error")
sys.exit(1)