587 lines
25 KiB
Python
587 lines
25 KiB
Python
from Scripts import utils, plist
|
|
import argparse, os
|
|
|
|
class PatchMerge:
|
|
def __init__(self, config=None, results=None, overwrite=False, interactive=True):
|
|
self.u = utils.Utils("Patch Merge")
|
|
self.w = 80
|
|
self.h = 24
|
|
self.red = "\u001b[41;1m"
|
|
self.yel = "\u001b[43;1m"
|
|
self.grn = "\u001b[42;1m"
|
|
self.blu = "\u001b[46;1m"
|
|
self.rst = "\u001b[0m"
|
|
self.copy_as_path = self.u.check_admin() if os.name=="nt" else False
|
|
if 2/3==0:
|
|
# ANSI escapes don't seem to work properly with python 2.x
|
|
self.red = self.yel = self.grn = self.blu = self.rst = ""
|
|
if os.name == "nt":
|
|
if 2/3!=0:
|
|
os.system("color") # Allow ANSI color escapes.
|
|
self.w = 120
|
|
self.h = 30
|
|
self.interactive = interactive
|
|
self.overwrite = overwrite
|
|
self.target_patches = (
|
|
("OpenCore","patches_OC.plist"),
|
|
("Clover","patches_Clover.plist")
|
|
)
|
|
self.config_path = config
|
|
self.config_type = None
|
|
self.output = results or self.get_default_results_folder()
|
|
# Expand paths as needed
|
|
if self.config_path:
|
|
self.config_path = os.path.realpath(self.config_path)
|
|
self.config_type,_,_ = self.get_plist_info(self.config_path)
|
|
if self.output:
|
|
self.output = os.path.realpath(self.output)
|
|
|
|
def _get_patches_plists(self, path):
|
|
# Append patches_OC/Clover.plist to the path, and return a list
|
|
# with the format:
|
|
# ((oc_path,exists,plist_name),(clover_path,exists,plist_name))
|
|
path_checks = []
|
|
for p_type,name in self.target_patches:
|
|
if path:
|
|
p = os.path.join(path,name)
|
|
isfile = os.path.isfile(p)
|
|
else:
|
|
p = None
|
|
isfile = False
|
|
path_checks.append((
|
|
p,
|
|
isfile,
|
|
name
|
|
))
|
|
return path_checks
|
|
|
|
def get_default_results_folder(self, prompt=False):
|
|
# Let's attempt to locate a Results folder either in the same
|
|
# directory as this script, or in the parent directory.
|
|
# If none is found - we'll have to prompt the user as needed.
|
|
#
|
|
# Try our directory first
|
|
local_path = os.path.dirname(os.path.realpath(__file__))
|
|
local_results = os.path.join(local_path,"Results")
|
|
parent_results = os.path.realpath(os.path.join(local_path,"..","Results"))
|
|
potentials = []
|
|
for path in (local_results,parent_results):
|
|
if os.path.isdir(path):
|
|
# Check if we have the files we need
|
|
o,c = self._get_patches_plists(path)
|
|
if o[1] or c[1]:
|
|
potentials.append(path)
|
|
if potentials:
|
|
return potentials[0]
|
|
# If we got here - we didn't find anything - check if we need
|
|
# to prompt
|
|
if not prompt:
|
|
# Nope - bail
|
|
return None
|
|
# We're prompting
|
|
return self.select_results_folder()
|
|
|
|
def select_results_folder(self):
|
|
while True:
|
|
self.u.head("Select Results Folder")
|
|
print("")
|
|
if self.copy_as_path:
|
|
print("NOTE: Currently running as admin on Windows - drag and drop may not work.")
|
|
print(" Shift + right-click in Explorer and select 'Copy as path' then paste here instead.")
|
|
print("")
|
|
print("M. Main Menu")
|
|
print("Q. Quit")
|
|
print("")
|
|
print("NOTE: This is the folder containing the patches_OC.plist and")
|
|
print(" patches_Clover.plist you are trying to merge. It will also be where")
|
|
print(" the patched config.plist is saved.")
|
|
print("")
|
|
path = self.u.grab("Please drag and drop the Results folder here: ")
|
|
if not path:
|
|
continue
|
|
if path.lower() == "m":
|
|
return self.output
|
|
elif path.lower() == "q":
|
|
self.u.custom_quit()
|
|
test_path = self.u.check_path(path)
|
|
if os.path.isfile(test_path):
|
|
# Got a file - get the containing folder
|
|
test_path = os.path.dirname(test_path)
|
|
if not test_path:
|
|
self.u.head("Invalid Path")
|
|
print("")
|
|
print("That path either does not exist, or is not a folder.")
|
|
print("")
|
|
self.u.grab("Press [enter] to return...")
|
|
continue
|
|
# Got a folder - check for patches_OC/Clover.plist
|
|
o,c = self._get_patches_plists(test_path)
|
|
if not (o[1] or c[1]):
|
|
# No patches plists in there
|
|
self.u.head("Missing Files")
|
|
print("")
|
|
print("Neither patches_OC.plist nor patches_Clover.plist were found at that path.")
|
|
print("")
|
|
self.u.grab("Press [enter] to return...")
|
|
continue
|
|
# We got what we need - set and return the path
|
|
self.output = test_path
|
|
return self.output
|
|
|
|
def get_ascii_print(self, data):
|
|
# Helper to sanitize unprintable characters by replacing them with
|
|
# ? where needed
|
|
unprintables = False
|
|
all_zeroes = True
|
|
ascii_string = ""
|
|
for b in data:
|
|
if not isinstance(b,int):
|
|
try: b = ord(b)
|
|
except: pass
|
|
if b != 0:
|
|
# Not wildcard matching
|
|
all_zeroes = False
|
|
if ord(" ") <= b < ord("~"):
|
|
ascii_string += chr(b)
|
|
else:
|
|
ascii_string += "?"
|
|
unprintables = True
|
|
return (False if all_zeroes else unprintables,ascii_string)
|
|
|
|
def check_normalize(self, patch_or_drop, normalize_headers, check_type="Patch"):
|
|
sig = ("OemTableId","TableSignature")
|
|
if normalize_headers:
|
|
# OpenCore - and NormalizeHeaders is enabled. Check if we have
|
|
# any unprintable ASCII chars in our OemTableId or TableSignature
|
|
# and warn.
|
|
if any(self.get_ascii_print(plist.extract_data(patch_or_drop.get(x,b"\x00")))[0] for x in sig):
|
|
print("\n{}!! WARNING !!{} NormalizeHeaders is {}ENABLED{}, and table ids contain unprintable".format(
|
|
self.yel,
|
|
self.rst,
|
|
self.grn,
|
|
self.rst
|
|
))
|
|
print(" characters! {} may not match or apply!\n".format(check_type))
|
|
return True
|
|
else:
|
|
# Not enabled - check for question marks as that may imply characters
|
|
# were sanitized when creating the patches/dropping tables.
|
|
if any(b"\x3F" in plist.extract_data(patch_or_drop.get(x,b"\x00")) for x in sig):
|
|
print("\n{}!! WARNING !!{} NormalizeHeaders is {}DISABLED{}, and table ids contain '?'!".format(
|
|
self.yel,
|
|
self.rst,
|
|
self.red,
|
|
self.rst
|
|
))
|
|
print(" {} may not match or apply!\n".format(check_type))
|
|
return True
|
|
return False
|
|
|
|
def ensure_path(self, plist_data, path_list, final_type = list):
|
|
if not path_list:
|
|
return plist_data
|
|
if not isinstance(plist_data,dict):
|
|
plist_data = {} # Override it with a dict
|
|
# Set our initial reference, then iterate the
|
|
# path list
|
|
last = plist_data
|
|
for i,path in enumerate(path_list,start=1):
|
|
# Check if our next path var is in last
|
|
if not path in last:
|
|
last[path] = {} if i < len(path_list) else final_type()
|
|
# Make sure it's the correct type if we're at the
|
|
# end of the entries
|
|
if i >= len(path_list) and not isinstance(last[path],final_type):
|
|
# Override it
|
|
last[path] = final_type()
|
|
# Update our reference
|
|
last = last[path]
|
|
return plist_data
|
|
|
|
def get_unique_name(self,name,target_folder,name_append=""):
|
|
# Get a new file name in the target folder so we don't override the original
|
|
name = os.path.basename(name)
|
|
ext = "" if not "." in name else name.split(".")[-1]
|
|
if ext: name = name[:-len(ext)-1]
|
|
if name_append: name = name+str(name_append)
|
|
check_name = ".".join((name,ext)) if ext else name
|
|
if not os.path.exists(os.path.join(target_folder,check_name)):
|
|
return check_name
|
|
# We need a unique name
|
|
num = 1
|
|
while True:
|
|
check_name = "{}-{}".format(name,num)
|
|
if ext: check_name += "."+ext
|
|
if not os.path.exists(os.path.join(target_folder,check_name)):
|
|
return check_name
|
|
num += 1 # Increment our counter
|
|
|
|
def pause_interactive(self, return_value=None):
|
|
if self.interactive:
|
|
print("")
|
|
self.u.grab("Press [enter] to return...")
|
|
return return_value
|
|
|
|
def patch_plist(self):
|
|
# Retain the config name
|
|
if self.interactive:
|
|
self.u.head("Patching Plist")
|
|
print("")
|
|
# Make sure we have a config_path
|
|
if not self.config_path:
|
|
print("No target plist path specified!")
|
|
return self.pause_interactive()
|
|
# Make sure that config_path exists
|
|
if not os.path.isfile(self.config_path):
|
|
print("Could not locate target plist at:")
|
|
print(" - {}".format(self.config_path))
|
|
return self.pause_interactive()
|
|
# Make sure our output var has a value
|
|
if not self.output:
|
|
print("No Results folder path specified!")
|
|
return self.pause_interactive()
|
|
config_name = os.path.basename(self.config_path)
|
|
print("Loading {}...".format(config_name))
|
|
self.config_type,config_data,e = self.get_plist_info(self.config_path)
|
|
if e:
|
|
print(" - Failed to load! {}".format(e))
|
|
return self.pause_interactive()
|
|
# Recheck the config.plist type
|
|
if not self.config_type:
|
|
print("Could not determine plist type!")
|
|
return self.pause_interactive()
|
|
# Ensure our patches plists exist, and break out info
|
|
# into the target_path and target_name as needed
|
|
target_path,_,target_name = self.get_patch_plist_for_type(
|
|
self.output,
|
|
self.config_type
|
|
)
|
|
# This should only show up if output is None/False/empty
|
|
if not target_path:
|
|
print("Could not locate {} in:".format(target_name or "the required patches plist"))
|
|
print(" - {}".format(self.output))
|
|
return self.pause_interactive()
|
|
# Make sure the path actually exists - and is a file
|
|
if not os.path.isfile(target_path):
|
|
print("Could not locate required patches at:")
|
|
print(" - {}".format(target_path))
|
|
return self.pause_interactive()
|
|
# Set up some preliminary variables for reporting later
|
|
errors_found = normalize_headers = False # Default to off
|
|
target_name = os.path.basename(target_path)
|
|
print("Loading {}...".format(target_name))
|
|
# Load the target plist
|
|
_,target_data,e = self.get_plist_info(target_path)
|
|
if e:
|
|
print(" - Failed to load! {}".format(e))
|
|
return self.pause_interactive()
|
|
print("Ensuring paths in {} and {}...".format(config_name,target_name))
|
|
# Make sure all the needed values are there
|
|
if self.config_type == "OpenCore":
|
|
for p in (("ACPI","Add"),("ACPI","Delete"),("ACPI","Patch")):
|
|
print(" - {}...".format(" -> ".join(p)))
|
|
config_data = self.ensure_path(config_data,p)
|
|
target_data = self.ensure_path(target_data,p)
|
|
print(" - ACPI -> Quirks...")
|
|
config_data = self.ensure_path(config_data,("ACPI","Quirks"),final_type=dict)
|
|
normalize_headers = config_data["ACPI"]["Quirks"].get("NormalizeHeaders",False)
|
|
if not isinstance(normalize_headers,(bool)):
|
|
errors_found = True
|
|
print("\n{}!! WARNING !!{} ACPI -> Quirks -> NormalizeHeaders is malformed - assuming False".format(
|
|
self.yel,
|
|
self.rst
|
|
))
|
|
normalize_headers = False
|
|
# Set up our patch sources
|
|
ssdts = target_data["ACPI"]["Add"]
|
|
patch = target_data["ACPI"]["Patch"]
|
|
drops = target_data["ACPI"]["Delete"]
|
|
# Set up our original values
|
|
s_orig = config_data["ACPI"]["Add"]
|
|
p_orig = config_data["ACPI"]["Patch"]
|
|
d_orig = config_data["ACPI"]["Delete"]
|
|
else:
|
|
for p in (("ACPI","DropTables"),("ACPI","SortedOrder"),("ACPI","DSDT","Patches")):
|
|
print(" - {}...".format(" -> ".join(p)))
|
|
config_data = self.ensure_path(config_data,p)
|
|
target_data = self.ensure_path(target_data,p)
|
|
# Set up our patch sources
|
|
ssdts = target_data["ACPI"]["SortedOrder"]
|
|
patch = target_data["ACPI"]["DSDT"]["Patches"]
|
|
drops = target_data["ACPI"]["DropTables"]
|
|
# Set up our original values
|
|
s_orig = config_data["ACPI"]["SortedOrder"]
|
|
p_orig = config_data["ACPI"]["DSDT"]["Patches"]
|
|
d_orig = config_data["ACPI"]["DropTables"]
|
|
print("")
|
|
if not ssdts:
|
|
print("--- No SSDTs to add - skipping...")
|
|
else:
|
|
print("--- Walking target SSDTs ({:,} total)...".format(len(ssdts)))
|
|
s_rem = []
|
|
# Gather any entries broken from user error
|
|
s_broken = [x for x in s_orig if not isinstance(x,dict)] if self.config_type == "OpenCore" else []
|
|
for s in ssdts:
|
|
if self.config_type == "OpenCore":
|
|
print(" - Checking {}...".format(s["Path"]))
|
|
existing = [x for x in s_orig if isinstance(x,dict) and x["Path"] == s["Path"]]
|
|
else:
|
|
print(" - Checking {}...".format(s))
|
|
existing = [x for x in s_orig if x == s]
|
|
if existing:
|
|
print(" --> Located {:,} existing to replace...".format(len(existing)))
|
|
s_rem.extend(existing)
|
|
if s_rem:
|
|
print(" - Removing {:,} existing duplicate{}...".format(len(s_rem),"" if len(s_rem)==1 else "s"))
|
|
for r in s_rem:
|
|
if r in s_orig: s_orig.remove(r)
|
|
else:
|
|
print(" - No duplicates to remove...")
|
|
print(" - Adding {:,} SSDT{}...".format(len(ssdts),"" if len(ssdts)==1 else "s"))
|
|
s_orig.extend(ssdts)
|
|
if s_broken:
|
|
errors_found = True
|
|
print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format(
|
|
self.yel,
|
|
self.rst,
|
|
len(s_broken),
|
|
"y" if len(d_broken)==1 else "ies",
|
|
config_name
|
|
))
|
|
print("")
|
|
if not patch:
|
|
print("--- No patches to add - skipping...")
|
|
else:
|
|
print("--- Walking target patches ({:,} total)...".format(len(patch)))
|
|
p_rem = []
|
|
# Gather any entries broken from user error
|
|
p_broken = [x for x in p_orig if not isinstance(x,dict)]
|
|
for p in patch:
|
|
print(" - Checking {}...".format(p["Comment"]))
|
|
if self.config_type == "OpenCore" and self.check_normalize(p,normalize_headers):
|
|
errors_found = True
|
|
existing = [x for x in p_orig if isinstance(x,dict) and x["Find"] == p["Find"] and x["Replace"] == p["Replace"]]
|
|
if existing:
|
|
print(" --> Located {:,} existing to replace...".format(len(existing)))
|
|
p_rem.extend(existing)
|
|
# Remove any dupes
|
|
if p_rem:
|
|
print(" - Removing {:,} existing duplicate{}...".format(len(p_rem),"" if len(p_rem)==1 else "s"))
|
|
for r in p_rem:
|
|
if r in p_orig: p_orig.remove(r)
|
|
else:
|
|
print(" - No duplicates to remove...")
|
|
print(" - Adding {:,} patch{}...".format(len(patch),"" if len(patch)==1 else "es"))
|
|
p_orig.extend(patch)
|
|
if p_broken:
|
|
errors_found = True
|
|
print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format(
|
|
self.yel,
|
|
self.rst,
|
|
len(p_broken),
|
|
"y" if len(d_broken)==1 else "ies",
|
|
config_name
|
|
))
|
|
print("")
|
|
if not drops:
|
|
print("--- No tables to drop - skipping...")
|
|
else:
|
|
print("--- Walking target tables to drop ({:,} total)...".format(len(drops)))
|
|
d_rem = []
|
|
# Gather any entries broken from user error
|
|
d_broken = [x for x in d_orig if not isinstance(x,dict)]
|
|
for d in drops:
|
|
if self.config_type == "OpenCore":
|
|
print(" - Checking {}...".format(d["Comment"]))
|
|
if self.check_normalize(d,normalize_headers,check_type="Dropped table"):
|
|
errors_found = True
|
|
existing = [x for x in d_orig if isinstance(x,dict) and x["TableSignature"] == d["TableSignature"] and x["OemTableId"] == d["OemTableId"]]
|
|
else:
|
|
name = " - ".join([x for x in (d.get("Signature",""),d.get("TableId","")) if x]) or "Unknown Dropped Table"
|
|
print(" - Checking {}...".format(name))
|
|
existing = [x for x in d_orig if isinstance(x,dict) and x.get("Signature") == d.get("Signature") and x.get("TableId") == d.get("TableId")]
|
|
if existing:
|
|
print(" --> Located {:,} existing to replace...".format(len(existing)))
|
|
d_rem.extend(existing)
|
|
if d_rem:
|
|
print(" - Removing {:,} existing duplicate{}...".format(len(d_rem),"" if len(d_rem)==1 else "s"))
|
|
for r in d_rem:
|
|
if r in d_orig: d_orig.remove(r)
|
|
else:
|
|
print(" - No duplicates to remove...")
|
|
print(" - Dropping {:,} table{}...".format(len(drops),"" if len(drops)==1 else "s"))
|
|
d_orig.extend(drops)
|
|
if d_broken:
|
|
errors_found = True
|
|
print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format(
|
|
self.yel,
|
|
self.rst,
|
|
len(d_broken),
|
|
"y" if len(d_broken)==1 else "ies",
|
|
config_name
|
|
))
|
|
print("")
|
|
if self.overwrite:
|
|
output_path = self.config_path
|
|
else:
|
|
config_name = self.get_unique_name(config_name,self.output)
|
|
output_path = os.path.join(self.output,config_name)
|
|
print("Saving to {}...".format(output_path))
|
|
try:
|
|
plist.dump(config_data,open(output_path,"wb"))
|
|
except Exception as e:
|
|
print(" - Failed to save! {}".format(e))
|
|
return self.pause_interactive()
|
|
print(" - Saved.")
|
|
print("")
|
|
if errors_found:
|
|
print("{}!! WARNING !!{} Potential errors were found when merging - please address them!".format(
|
|
self.yel,
|
|
self.rst
|
|
))
|
|
print("")
|
|
if not self.overwrite:
|
|
print("{}!! WARNING !!{} Make sure you review the saved {} before replacing!".format(
|
|
self.red,
|
|
self.rst,
|
|
config_name
|
|
))
|
|
print("")
|
|
print("Done.")
|
|
return self.pause_interactive()
|
|
|
|
def get_plist_info(self, config_path):
|
|
# Attempts to load the passed config and return a tuple
|
|
# of (type_string,config_data,error)
|
|
type_string = config_data = e = None
|
|
try:
|
|
config_data = plist.load(open(config_path,"rb"))
|
|
except Exception as e:
|
|
return (None,None,e)
|
|
if not isinstance(config_data,dict):
|
|
e = "Invalid root node type: {}".format(type(config_data))
|
|
else:
|
|
type_string = "OpenCore" if "PlatformInfo" in config_data else "Clover" if "SMBIOS" in config_data else None
|
|
return (type_string,config_data,None)
|
|
|
|
def get_patch_plist_for_type(self, path, config_type):
|
|
o,c = self._get_patches_plists(path)
|
|
return {
|
|
"OpenCore":o,
|
|
"Clover":c
|
|
}.get(config_type,(None,False,None))
|
|
|
|
def select_plist(self):
|
|
while True:
|
|
self.u.head("Select Plist")
|
|
print("")
|
|
if self.copy_as_path:
|
|
print("NOTE: Currently running as admin on Windows - drag and drop may not work.")
|
|
print(" Shift + right-click in Explorer and select 'Copy as path' then paste here instead.")
|
|
print("")
|
|
print("M. Main Menu")
|
|
print("Q. Quit")
|
|
print("")
|
|
path = self.u.grab("Please drag and drop the config.plist here: ")
|
|
if not path: continue
|
|
if path.lower() == "m": return
|
|
elif path.lower() == "q": self.u.custom_quit()
|
|
test_path = self.u.check_path(path)
|
|
if not test_path or not os.path.isfile(test_path):
|
|
self.u.head("Invalid Path")
|
|
print("")
|
|
print("That path either does not exist, or is not a file.")
|
|
print("")
|
|
self.u.grab("Press [enter] to return...")
|
|
continue
|
|
# Got a file - try to load it
|
|
t,_,e = self.get_plist_info(test_path)
|
|
if e:
|
|
self.u.head("Invalid File")
|
|
print("")
|
|
print("That file failed to load:\n\n{}".format(e))
|
|
print("")
|
|
self.u.grab("Press [enter] to return...")
|
|
continue
|
|
# Got a valid file
|
|
self.config_path = test_path
|
|
self.config_type = t
|
|
return
|
|
|
|
def main(self):
|
|
# Gather some preliminary info for display
|
|
target_path,target_exists,target_name = self.get_patch_plist_for_type(
|
|
self.output,
|
|
self.config_type
|
|
)
|
|
self.u.resize(self.w,self.h)
|
|
self.u.head()
|
|
print("")
|
|
print("Current config.plist: {}".format(self.config_path))
|
|
print("Type of config.plist: {}".format(self.config_type or "Unknown"))
|
|
print("Results Folder: {}".format(self.output))
|
|
print("Patches Plist: {}{}".format(
|
|
target_name or "Unknown",
|
|
"" if (not target_name or target_exists) else " - {}!! MISSING !!{}".format(self.red,self.rst)
|
|
))
|
|
print("Overwrite Original: {}{}{}{}".format(
|
|
self.red if self.overwrite else self.grn,
|
|
"!! True !!" if self.overwrite else "False",
|
|
self.rst,
|
|
" - Make Sure You Have A Backup!" if self.overwrite else ""
|
|
))
|
|
print("")
|
|
print("C. Select config.plist")
|
|
print("O. Toggle Overwrite Original")
|
|
print("R. Select Results Folder")
|
|
if self.config_path and target_exists:
|
|
print("P. Patch with {}".format(target_name))
|
|
print("")
|
|
print("Q. Quit")
|
|
print("")
|
|
menu = self.u.grab("Please make a selection: ")
|
|
if not len(menu):
|
|
return
|
|
if menu.lower() == "q":
|
|
self.u.custom_quit()
|
|
elif menu.lower() == "c":
|
|
self.select_plist()
|
|
elif menu.lower() == "o":
|
|
self.overwrite ^= True
|
|
elif menu.lower() == "r":
|
|
self.select_results_folder()
|
|
elif menu.lower() == "p" and self.config_path and target_exists:
|
|
self.patch_plist()
|
|
|
|
if __name__ == '__main__':
|
|
# Setup the cli args
|
|
parser = argparse.ArgumentParser(prog="PatchMerge.py", description="PatchMerge - py script to merge patches_[OC/Clover].plist with a config.plist.")
|
|
parser.add_argument("-c", "--config", help="path to target config.plist - required if running in non-interactive mode")
|
|
parser.add_argument("-r", "--results", help="path to Results folder containing patches_[OC/Clover].plist - required if running in non-interactive mode")
|
|
parser.add_argument("-o", "--overwrite", help="overwrite the original config.plist", action="store_true")
|
|
parser.add_argument("-i", "--no-interaction", help="run in non-interactive mode - requires -c and -r", action="store_true")
|
|
|
|
args = parser.parse_args()
|
|
|
|
p = PatchMerge(
|
|
config=args.config,
|
|
results=args.results,
|
|
overwrite=args.overwrite,
|
|
interactive=not args.no_interaction
|
|
)
|
|
|
|
if args.no_interaction:
|
|
# We're in non-interactive mode here
|
|
p.patch_plist()
|
|
else:
|
|
# Interactive mode
|
|
if 2/3 == 0:
|
|
input = raw_input
|
|
while True:
|
|
try:
|
|
p.main()
|
|
except Exception as e:
|
|
print("An error occurred: {}".format(e))
|
|
print("")
|
|
input("Press [enter] to continue...")
|