From 573f5fc375cb52688ccac4312de8422ae263dcf2 Mon Sep 17 00:00:00 2001 From: jaminmc <1310376+jaminmc@users.noreply.github.com> Date: Sun, 20 Jul 2025 11:15:14 -0400 Subject: [PATCH 1/2] Added a CLI interface. Added a CLI interface, so this can be used for scripts. Here is a sample: ``` (base) user@users-MacBook-Pro-M1-2 GenSMBIOS % python3 GenSMBIOS.py -h usage: GenSMBIOS.py [-h] [--install] [--plist PLIST] [--plist-type {clover,opencore}] [--generate GENERATE [GENERATE ...]] [--uuid] [--rom] [--list] [--toggle-rom] [--args ARGS] [--clear-args] [--version] [-j JSON] GenSMBIOS CLI options: -h, --help show this help message and exit --install Install/Update MacSerial --plist PLIST Path to config.plist --plist-type {clover,opencore} Specify plist type if not auto-detected --generate GENERATE [GENERATE ...] Generate SMBIOS: [times] (e.g., iMac18,3 5) --uuid Generate UUID --rom Generate ROM --list List current SMBIOS --toggle-rom Toggle generate ROM with SMBIOS --args ARGS Set additional args for macserial --clear-args Clear additional args for macserial --version Show MacSerial version -j JSON, --json JSON Export generated SMBIOS to JSON file % python3 GenSMBIOS.py --plist ~/Documents/OCC/EfiMaker/2025-07-19-22h39m38s/EFI/OC/config.plist -j /tmp/test.json --generate iMac18,3 1 ####################################################### # Getting MacSerial Remote Version # ####################################################### Gathering latest macserial info... - Gathering info from OpenCorePkg... iMac18,3 SMBIOS Info Type: iMac18,3 Serial: C02YL8YJJ1GJ Board Serial: C029164054NJ0PGJA SmUUID: D74CF4E7-536D-43C2-9FAE-A23D26661530 Apple ROM: 24240E5554D7 Flushing SMBIOS entry to /Users/user/Documents/OCC/EfiMaker/2025-07-19-22h39m38s/EFI/OC/config.plist Exported to /tmp/test.json (base) user@users-MacBook-Pro-M1-2 GenSMBIOS % cat /tmp/test.json { "Type": "iMac18,3", "Serial": "C02YL8YJJ1GJ", "Board Serial": "C029164054NJ0PGJA", "SmUUID": "D74CF4E7-536D-43C2-9FAE-A23D26661530", "ROM": "24240E5554D7" } ``` --- GenSMBIOS.py | 456 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 381 insertions(+), 75 deletions(-) diff --git a/GenSMBIOS.py b/GenSMBIOS.py index 26fbedf..9b80570 100755 --- a/GenSMBIOS.py +++ b/GenSMBIOS.py @@ -2,28 +2,34 @@ import os, subprocess, shlex, sys, tempfile, shutil, random, uuid, zipfile, json, binascii from Scripts import downloader, plist, run, utils from collections import OrderedDict + # Import from secrets - or fall back on random.SystemRandom() # functions if on python 2 try: from secrets import randbits, choice + basestring = str except ImportError: from random import SystemRandom + _sysrand = SystemRandom() randbits = _sysrand.getrandbits choice = _sysrand.choice + class Smbios: def __init__(self): os.chdir(os.path.dirname(os.path.realpath(__file__))) self.u = utils.Utils("GenSMBIOS") self.d = downloader.Downloader() self.r = run.Run() - self.oc_release_url = "https://github.com/acidanthera/OpenCorePkg/releases/latest" + self.oc_release_url = ( + "https://github.com/acidanthera/OpenCorePkg/releases/latest" + ) self.scripts = "Scripts" self.plist = None self.plist_data = None - self.plist_type = "Unknown" # Can be "Clover" or "OpenCore" depending + self.plist_type = "Unknown" # Can be "Clover" or "OpenCore" depending self.remote = self._get_remote_version() self.okay_keys = [ "SerialNumber", @@ -31,19 +37,25 @@ def __init__(self): "SmUUID", "ProductName", "Trust", - "Memory" + "Memory", ] - try: self.rom_prefixes = json.load(open(os.path.join(self.scripts,"prefix.json"))) - except: self.rom_prefixes = [] - self.settings_file = os.path.join(self.scripts,"settings.json") - try: self.settings = json.load(open(self.settings_file)) - except: self.settings = {} + try: + self.rom_prefixes = json.load( + open(os.path.join(self.scripts, "prefix.json")) + ) + except: + self.rom_prefixes = [] + self.settings_file = os.path.join(self.scripts, "settings.json") + try: + self.settings = json.load(open(self.settings_file)) + except: + self.settings = {} self.gen_rom = True def _save_settings(self): if self.settings: try: - json.dump(self.settings,open(self.settings_file,"w"),indent=2) + json.dump(self.settings, open(self.settings_file, "w"), indent=2) except: pass elif os.path.exists(self.settings_file): @@ -61,9 +73,13 @@ def _get_macserial_version(self): if "expanded_assets" in line: # Get the version from the URL oc_vers = line.split(' src="')[1].split('"')[0].split("/")[-1] - macserial_h_url = "https://raw.githubusercontent.com/acidanthera/OpenCorePkg/{}/Utilities/macserial/macserial.h".format(oc_vers) - macserial_h = self.d.get_string(macserial_h_url,False) - macserial_v = macserial_h.split('#define PROGRAM_VERSION "')[1].split('"')[0] + macserial_h_url = "https://raw.githubusercontent.com/acidanthera/OpenCorePkg/{}/Utilities/macserial/macserial.h".format( + oc_vers + ) + macserial_h = self.d.get_string(macserial_h_url, False) + macserial_v = macserial_h.split('#define PROGRAM_VERSION "')[ + 1 + ].split('"')[0] except: pass return macserial_v @@ -74,18 +90,33 @@ def _get_macserial_url(self): urlsource = self.d.get_string(self.oc_release_url, False) for line in urlsource.split("\n"): if "expanded_assets" in line: - expanded_html = self.d.get_string(line.split(' src="')[1].split('"')[0], False) + expanded_html = self.d.get_string( + line.split(' src="')[1].split('"')[0], False + ) for l in expanded_html.split("\n"): - if 'href="/acidanthera/OpenCorePkg/releases/download/' in l and "-RELEASE.zip" in l: + if ( + 'href="/acidanthera/OpenCorePkg/releases/download/' in l + and "-RELEASE.zip" in l + ): # Got it - return "https://github.com{}".format(l.split('href="')[1].split('"')[0]) + return "https://github.com{}".format( + l.split('href="')[1].split('"')[0] + ) except: pass return None - def _get_binary(self,binary_name=None): + def _get_binary(self, binary_name=None): if not binary_name: - binary_name = ["macserial.exe","macserial32.exe"] if os.name == "nt" else ["macserial.linux","macserial"] if sys.platform.startswith("linux") else ["macserial"] + binary_name = ( + ["macserial.exe", "macserial32.exe"] + if os.name == "nt" + else ( + ["macserial.linux", "macserial"] + if sys.platform.startswith("linux") + else ["macserial"] + ) + ) # Check locally cwd = os.getcwd() os.chdir(os.path.dirname(os.path.realpath(__file__))) @@ -94,20 +125,28 @@ def _get_binary(self,binary_name=None): if os.path.exists(name): path = os.path.join(os.getcwd(), name) elif os.path.exists(os.path.join(os.getcwd(), self.scripts, name)): - path = os.path.join(os.getcwd(),self.scripts,name) - if path: break # Found it, bail + path = os.path.join(os.getcwd(), self.scripts, name) + if path: + break # Found it, bail os.chdir(cwd) return path - def _get_version(self,macserial): + def _get_version(self, macserial): # Gets the macserial version - out, error, code = self.r.run({"args":[macserial]}) + out, error, code = self.r.run({"args": [macserial]}) if not len(out): return None for line in out.split("\n"): if not line.lower().startswith("version"): continue - vers = next((x for x in line.lower().strip().split() if len(x) and x[0] in "0123456789"),None) + vers = next( + ( + x + for x in line.lower().strip().split() + if len(x) and x[0] in "0123456789" + ), + None, + ) if not vers is None and vers[-1] == ".": vers = vers[:-1] return vers @@ -117,30 +156,33 @@ def _download_and_extract(self, temp, url, path_in_zip=[]): ztemp = tempfile.mkdtemp(dir=temp) zfile = os.path.basename(url) print("\nDownloading {}...".format(os.path.basename(url))) - result = self.d.stream_to_file(url, os.path.join(ztemp,zfile)) + result = self.d.stream_to_file(url, os.path.join(ztemp, zfile)) print("") if not result: raise Exception(" - Failed to download!") print(" - Extracting...") btemp = tempfile.mkdtemp(dir=temp) # Extract with built-in tools \o/ - with zipfile.ZipFile(os.path.join(ztemp,zfile)) as z: - z.extractall(os.path.join(temp,btemp)) - script_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),self.scripts) - search_path = os.path.join(temp,btemp) + with zipfile.ZipFile(os.path.join(ztemp, zfile)) as z: + z.extractall(os.path.join(temp, btemp)) + script_dir = os.path.join( + os.path.dirname(os.path.realpath(__file__)), self.scripts + ) + search_path = os.path.join(temp, btemp) # Extend the search path if path_in_zip contains elements - if path_in_zip: search_path = os.path.join(search_path,*path_in_zip) + if path_in_zip: + search_path = os.path.join(search_path, *path_in_zip) for x in os.listdir(search_path): if "macserial" in x.lower(): # Found one print(" - Found {}".format(x)) if os.name != "nt": print(" - Chmod +x...") - self.r.run({"args":["chmod","+x",os.path.join(search_path,x)]}) + self.r.run({"args": ["chmod", "+x", os.path.join(search_path, x)]}) print(" - Copying to {} directory...".format(self.scripts)) if not os.path.exists(script_dir): os.mkdir(script_dir) - shutil.copy(os.path.join(search_path,x), os.path.join(script_dir,x)) + shutil.copy(os.path.join(search_path, x), os.path.join(script_dir, x)) def _get_macserial(self): # Download both the windows and mac versions of macserial and expand them to the Scripts dir @@ -148,22 +190,22 @@ def _get_macserial(self): print("") print("Gathering latest macserial info...") url = self._get_macserial_url() - path_in_zip = ["Utilities","macserial"] + path_in_zip = ["Utilities", "macserial"] if not url: print("Error checking for updates (network issue)\n") self.u.grab("Press [enter] to return...") return temp = tempfile.mkdtemp() - cwd = os.getcwd() + cwd = os.getcwd() try: print(" - {}".format(url)) - self._download_and_extract(temp,url,path_in_zip) + self._download_and_extract(temp, url, path_in_zip) except Exception as e: print("We ran into some problems :(\n\n{}".format(e)) print("\nCleaning up...") os.chdir(cwd) shutil.rmtree(temp) - self.u.grab("\nDone.",timeout=5) + self.u.grab("\nDone.", timeout=5) return def _get_remote_version(self): @@ -197,7 +239,7 @@ def _get_plist(self): self.plist = None self.plist_data = None return - + pc = self.u.check_path(p) if not pc: self.u.head("File Missing") @@ -208,7 +250,7 @@ def _get_plist(self): return self._get_plist() try: with open(pc, "rb") as f: - self.plist_data = plist.load(f,dict_type=OrderedDict) + self.plist_data = plist.load(f, dict_type=OrderedDict) except Exception as e: self.u.head("Plist Malformed") print("") @@ -217,7 +259,11 @@ def _get_plist(self): self.u.grab("Press [enter] to return...") return self._get_plist() # Got a valid plist - let's try to check for Clover or OC structure - detected_type = "OpenCore" if "PlatformInfo" in self.plist_data else "Clover" if "SMBIOS" in self.plist_data else "Unknown" + detected_type = ( + "OpenCore" + if "PlatformInfo" in self.plist_data + else "Clover" if "SMBIOS" in self.plist_data else "Unknown" + ) if detected_type.lower() == "unknown": # Have the user decide which to do while True: @@ -231,8 +277,9 @@ def _get_plist(self): print("M. Return to the Menu") print("") t = self.u.grab("Please select the target type: ").lower() - if t == "m": return self._get_plist() - elif t in ("1","2"): + if t == "m": + return self._get_plist() + elif t in ("1", "2"): detected_type = "Clover" if t == "1" else "OpenCore" break # Got a plist and type - let's save it @@ -240,7 +287,7 @@ def _get_plist(self): # Apply any key-stripping or safety checks if self.plist_type.lower() == "clover": # Got a valid clover plist - let's check keys - key_check = self.plist_data.get("SMBIOS",{}) + key_check = self.plist_data.get("SMBIOS", {}) new_smbios = {} removed_keys = [] for key in key_check: @@ -250,19 +297,25 @@ def _get_plist(self): # Build our new SMBIOS new_smbios[key] = key_check[key] # We want the SmUUID to be the top-level - remove CustomUUID if exists - if "CustomUUID" in self.plist_data.get("SystemParameters",{}): + if "CustomUUID" in self.plist_data.get("SystemParameters", {}): removed_keys.append("CustomUUID") if len(removed_keys): while True: self.u.head("") print("") - print("The following keys will be removed:\n\n{}\n".format(", ".join(removed_keys))) + print( + "The following keys will be removed:\n\n{}\n".format( + ", ".join(removed_keys) + ) + ) con = self.u.grab("Continue? (y/n): ") if con.lower() == "y": # Flush settings self.plist_data["SMBIOS"] = new_smbios # Remove the CustomUUID if present - self.plist_data.get("SystemParameters",{}).pop("CustomUUID", None) + self.plist_data.get("SystemParameters", {}).pop( + "CustomUUID", None + ) break elif con.lower() == "n": self.plist_data = None @@ -271,12 +324,12 @@ def _get_plist(self): def _get_rom(self): # Generate 6-bytes of cryptographically random values - rom_str = "{:x}".format(randbits(8*6)).upper().rjust(12,"0") + rom_str = "{:x}".format(randbits(8 * 6)).upper().rjust(12, "0") if self.rom_prefixes: # Replace the prefix with one from our list prefix = choice(self.rom_prefixes) - if isinstance(prefix,basestring): - rom_str = prefix+rom_str[len(prefix):] + if isinstance(prefix, basestring): + rom_str = prefix + rom_str[len(prefix) :] return rom_str def _get_smbios(self, macserial, smbios_type, times=1): @@ -284,10 +337,13 @@ def _get_smbios(self, macserial, smbios_type, times=1): total = [] # Get any additional args and ensure they're a string args = self.settings.get("macserial_args") - if not isinstance(args,basestring): args = "" + if not isinstance(args, basestring): + args = "" while len(total) < times: total_len = len(total) - smbios, err, code = self.r.run({"args":[macserial,"-a"]+shlex.split(args)}) + smbios, err, code = self.r.run( + {"args": [macserial, "-a"] + shlex.split(args)} + ) if code != 0: # Issues generating return None @@ -342,11 +398,11 @@ def _generate_smbios(self, macserial): if len(menu) == 1: # Default of one time smtype = menu[0] - times = 1 + times = 1 else: smtype = menu[0] try: - times = int(menu[1]) + times = int(menu[1]) except: self.u.head("Incorrect Input") print("") @@ -360,7 +416,7 @@ def _generate_smbios(self, macserial): times = 1 if times > 20: times = 20 - smbios = self._get_smbios(macserial,smtype,times) + smbios = self._get_smbios(macserial, smtype, times) if smbios is None: # Issues generating print("Error - macserial returned an error!") @@ -372,12 +428,19 @@ def _generate_smbios(self, macserial): return self.u.head("{} SMBIOS Info".format(smbios[0][0])) print("") - if self.settings.get("macserial_args") and isinstance(self.settings["macserial_args"],basestring): + if self.settings.get("macserial_args") and isinstance( + self.settings["macserial_args"], basestring + ): print("Additional Arguments Passed:") print(" {}".format(self.settings["macserial_args"])) print("") - f_string = "Type: {}\nSerial: {}\nBoard Serial: {}\nSmUUID: {}" - if self.gen_rom: f_string += "\nApple ROM: {}" if self.rom_prefixes else "\nRandom ROM: {}" + f_string = ( + "Type: {}\nSerial: {}\nBoard Serial: {}\nSmUUID: {}" + ) + if self.gen_rom: + f_string += ( + "\nApple ROM: {}" if self.rom_prefixes else "\nRandom ROM: {}" + ) print("\n\n".join([f_string.format(*x) for x in smbios])) if self.plist_data and self.plist and os.path.exists(self.plist): # Let's apply - got a valid file, and plist data @@ -387,7 +450,7 @@ def _generate_smbios(self, macserial): print("\nFlushing SMBIOS entry to {}".format(self.plist)) if self.plist_type.lower() == "clover": # Ensure plist data exists - for x in ["SMBIOS","RtVariables","SystemParameters"]: + for x in ["SMBIOS", "RtVariables", "SystemParameters"]: if not x in self.plist_data: self.plist_data[x] = {} self.plist_data["SMBIOS"]["ProductName"] = smbios[0][0] @@ -396,19 +459,29 @@ def _generate_smbios(self, macserial): self.plist_data["RtVariables"]["MLB"] = smbios[0][2] self.plist_data["SMBIOS"]["SmUUID"] = smbios[0][3] if self.gen_rom: - self.plist_data["RtVariables"]["ROM"] = plist.wrap_data(binascii.unhexlify(smbios[0][4].encode("utf-8"))) + self.plist_data["RtVariables"]["ROM"] = plist.wrap_data( + binascii.unhexlify(smbios[0][4].encode("utf-8")) + ) self.plist_data["SystemParameters"]["InjectSystemID"] = True elif self.plist_type.lower() == "opencore": # Ensure data exists - if not "PlatformInfo" in self.plist_data: self.plist_data["PlatformInfo"] = {} - if not "Generic" in self.plist_data["PlatformInfo"]: self.plist_data["PlatformInfo"]["Generic"] = {} + if not "PlatformInfo" in self.plist_data: + self.plist_data["PlatformInfo"] = {} + if not "Generic" in self.plist_data["PlatformInfo"]: + self.plist_data["PlatformInfo"]["Generic"] = {} # Set the values - self.plist_data["PlatformInfo"]["Generic"]["SystemProductName"] = smbios[0][0] - self.plist_data["PlatformInfo"]["Generic"]["SystemSerialNumber"] = smbios[0][1] + self.plist_data["PlatformInfo"]["Generic"]["SystemProductName"] = ( + smbios[0][0] + ) + self.plist_data["PlatformInfo"]["Generic"]["SystemSerialNumber"] = ( + smbios[0][1] + ) self.plist_data["PlatformInfo"]["Generic"]["MLB"] = smbios[0][2] self.plist_data["PlatformInfo"]["Generic"]["SystemUUID"] = smbios[0][3] if self.gen_rom: - self.plist_data["PlatformInfo"]["Generic"]["ROM"] = plist.wrap_data(binascii.unhexlify(smbios[0][4].encode("utf-8"))) + self.plist_data["PlatformInfo"]["Generic"]["ROM"] = plist.wrap_data( + binascii.unhexlify(smbios[0][4].encode("utf-8")) + ) with open(self.plist, "wb") as f: plist.dump(self.plist_data, f, sort_keys=False) # Got only valid keys now @@ -423,8 +496,14 @@ def _list_current(self, macserial): print("") self.u.grab("Press [enter] to return...") return - out, err, code = self.r.run({"args":[macserial]}) - out = "\n".join([x for x in out.split("\n") if not x.lower().startswith("version") and len(x)]) + out, err, code = self.r.run({"args": [macserial]}) + out = "\n".join( + [ + x + for x in out.split("\n") + if not x.lower().startswith("version") and len(x) + ] + ) self.u.head("Current SMBIOS Info") print("") print(out) @@ -437,10 +516,13 @@ def get_additional_args(self): print("") print("Current Additional Arguments:") args = self.settings.get("macserial_args") - if not args or not isinstance(args,basestring): args = None + if not args or not isinstance(args, basestring): + args = None print(" {}".format(args)) print("") - print("The -a argument is always passed to macserial, but you can enter additional") + print( + "The -a argument is always passed to macserial, but you can enter additional" + ) print("arguments to fine-tune SMBIOS generation.") print("") print("C. Clear Additional Arguments") @@ -455,7 +537,7 @@ def get_additional_args(self): elif args.lower() == "q": self.u.custom_quit() elif args.lower() == "c": - self.settings.pop("macserial_args",None) + self.settings.pop("macserial_args", None) self._save_settings() else: self.settings["macserial_args"] = args @@ -483,9 +565,14 @@ def main(self): print("4. Generate UUID") print("5. Generate ROM") print("6. List Current SMBIOS") - print("7. Generate ROM With SMBIOS (Currently {})".format("Enabled" if self.gen_rom else "Disabled")) + print( + "7. Generate ROM With SMBIOS (Currently {})".format( + "Enabled" if self.gen_rom else "Disabled" + ) + ) args = self.settings.get("macserial_args") - if not args or not isinstance(args,basestring): args = None + if not args or not isinstance(args, basestring): + args = None print("8. Additional Args (Currently: {})".format(args)) print("") print("Q. Quit") @@ -510,7 +597,11 @@ def main(self): elif menu == "5": self.u.head("Generated ROM") print("") - print("{} ROM: {}".format("Apple" if self.rom_prefixes else "Random", self._get_rom())) + print( + "{} ROM: {}".format( + "Apple" if self.rom_prefixes else "Random", self._get_rom() + ) + ) print("") self.u.grab("Press [enter] to return...") elif menu == "6": @@ -520,14 +611,229 @@ def main(self): elif menu == "8": self.get_additional_args() + if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="GenSMBIOS CLI") + parser.add_argument( + "--install", action="store_true", help="Install/Update MacSerial" + ) + parser.add_argument("--plist", type=str, help="Path to config.plist") + parser.add_argument( + "--plist-type", + choices=["clover", "opencore"], + help="Specify plist type if not auto-detected", + ) + parser.add_argument( + "--generate", + nargs="+", + help="Generate SMBIOS: [times] (e.g., iMac18,3 5)", + ) + parser.add_argument("--uuid", action="store_true", help="Generate UUID") + parser.add_argument("--rom", action="store_true", help="Generate ROM") + parser.add_argument("--list", action="store_true", help="List current SMBIOS") + parser.add_argument( + "--toggle-rom", action="store_true", help="Toggle generate ROM with SMBIOS" + ) + parser.add_argument("--args", type=str, help="Set additional args for macserial") + parser.add_argument( + "--clear-args", action="store_true", help="Clear additional args for macserial" + ) + parser.add_argument("--version", action="store_true", help="Show MacSerial version") + parser.add_argument( + "-j", "--json", type=str, help="Export generated SMBIOS to JSON file" + ) + args = parser.parse_args() + s = Smbios() - while True: + processed = False + + if args.install: + s._get_macserial() + processed = True + + if args.plist: + pc = s.u.check_path(args.plist) + if not pc: + print("Plist file not found: {}".format(args.plist)) + sys.exit(1) try: - s.main() + with open(pc, "rb") as f: + s.plist_data = plist.load(f, dict_type=OrderedDict) except Exception as e: - print(e) - if sys.version_info >= (3, 0): - input("Press [enter] to return...") + print("Plist malformed: {}".format(e)) + sys.exit(1) + detected_type = ( + "OpenCore" + if "PlatformInfo" in s.plist_data + else "Clover" if "SMBIOS" in s.plist_data else "Unknown" + ) + if detected_type == "Unknown": + if args.plist_type: + detected_type = args.plist_type.capitalize() else: - raw_input("Press [enter] to return...") + print("Could not determine plist type! Use --plist-type to specify.") + sys.exit(1) + if detected_type not in ("Clover", "OpenCore"): + print("Invalid plist type: {}".format(detected_type)) + sys.exit(1) + s.plist_type = detected_type + if s.plist_type == "Clover": + key_check = s.plist_data.get("SMBIOS", {}) + new_smbios = {k: v for k, v in key_check.items() if k in s.okay_keys} + removed_keys = [k for k in key_check if k not in s.okay_keys] + if "CustomUUID" in s.plist_data.get("SystemParameters", {}): + removed_keys.append("CustomUUID") + s.plist_data["SystemParameters"].pop("CustomUUID", None) + if removed_keys: + print("Removed keys from plist: {}".format(", ".join(removed_keys))) + s.plist_data["SMBIOS"] = new_smbios + s.plist = pc + processed = True + + if args.clear_args: + s.settings.pop("macserial_args", None) + s._save_settings() + processed = True + + if args.args is not None: + s.settings["macserial_args"] = args.args + s._save_settings() + processed = True + + if args.toggle_rom: + s.gen_rom = not s.gen_rom + print( + "Generate ROM with SMBIOS: {}".format( + "Enabled" if s.gen_rom else "Disabled" + ) + ) + processed = True + + if args.generate: + macserial = s._get_binary() + if not macserial: + print("MacSerial not found! Use --install to download.") + sys.exit(1) + menu = args.generate + smtype = menu[0] + times = 1 if len(menu) < 2 else int(menu[1]) + if times < 1: + times = 1 + if times > 20: + times = 20 + smbios = s._get_smbios(macserial, smtype, times) + if smbios is None: + print("Error - macserial returned an error!") + sys.exit(1) + if smbios == False: + print("Error - {} not generated by macserial".format(smtype)) + sys.exit(1) + print("{} SMBIOS Info".format(smbios[0][0])) + if s.settings.get("macserial_args"): + print( + "Additional Arguments Passed: {}".format(s.settings["macserial_args"]) + ) + f_string = ( + "Type: {}\nSerial: {}\nBoard Serial: {}\nSmUUID: {}" + ) + if s.gen_rom: + f_string += "\nApple ROM: {}" if s.rom_prefixes else "\nRandom ROM: {}" + print("\n\n".join([f_string.format(*x) for x in smbios])) + if s.plist_data and s.plist: + if len(smbios) > 1: + print("\nFlushing first SMBIOS entry to {}".format(s.plist)) + else: + print("\nFlushing SMBIOS entry to {}".format(s.plist)) + if s.plist_type == "Clover": + for x in ["SMBIOS", "RtVariables", "SystemParameters"]: + if x not in s.plist_data: + s.plist_data[x] = {} + s.plist_data["SMBIOS"]["ProductName"] = smbios[0][0] + s.plist_data["SMBIOS"]["SerialNumber"] = smbios[0][1] + s.plist_data["SMBIOS"]["BoardSerialNumber"] = smbios[0][2] + s.plist_data["RtVariables"]["MLB"] = smbios[0][2] + s.plist_data["SMBIOS"]["SmUUID"] = smbios[0][3] + if s.gen_rom: + s.plist_data["RtVariables"]["ROM"] = plist.wrap_data( + binascii.unhexlify(smbios[0][4].encode("utf-8")) + ) + s.plist_data["SystemParameters"]["InjectSystemID"] = True + elif s.plist_type == "OpenCore": + if "PlatformInfo" not in s.plist_data: + s.plist_data["PlatformInfo"] = {} + if "Generic" not in s.plist_data["PlatformInfo"]: + s.plist_data["PlatformInfo"]["Generic"] = {} + s.plist_data["PlatformInfo"]["Generic"]["SystemProductName"] = smbios[ + 0 + ][0] + s.plist_data["PlatformInfo"]["Generic"]["SystemSerialNumber"] = smbios[ + 0 + ][1] + s.plist_data["PlatformInfo"]["Generic"]["MLB"] = smbios[0][2] + s.plist_data["PlatformInfo"]["Generic"]["SystemUUID"] = smbios[0][3] + if s.gen_rom: + s.plist_data["PlatformInfo"]["Generic"]["ROM"] = plist.wrap_data( + binascii.unhexlify(smbios[0][4].encode("utf-8")) + ) + with open(s.plist, "wb") as f: + plist.dump(s.plist_data, f, sort_keys=False) + if args.json: + data = [] + for x in smbios: + d = {"Type": x[0], "Serial": x[1], "Board Serial": x[2], "SmUUID": x[3]} + if len(x) > 4: + d["ROM"] = x[4] + data.append(d) + with open(args.json, "w") as f: + json.dump(data if len(data) > 1 else data[0], f, indent=4) + print("\nExported to {}".format(args.json)) + processed = True + + if args.uuid: + print(str(uuid.uuid4()).upper()) + processed = True + + if args.rom: + prefix = "Apple" if s.rom_prefixes else "Random" + print("{} ROM: {}".format(prefix, s._get_rom())) + processed = True + + if args.list: + macserial = s._get_binary() + if not macserial: + print("MacSerial not found! Use --install to download.") + sys.exit(1) + out, err, code = s.r.run({"args": [macserial]}) + out = "\n".join( + [ + x + for x in out.split("\n") + if not x.lower().startswith("version") and len(x) + ] + ) + print("Current SMBIOS Info") + print(out) + processed = True + + if args.version: + macserial = s._get_binary() + if macserial: + print("MacSerial v{}".format(s._get_version(macserial))) + else: + print("MacSerial not found!") + if s.remote: + print("Remote Version v{}".format(s.remote)) + processed = True + + if not processed: + while True: + try: + s.main() + except Exception as e: + print(e) + if sys.version_info >= (3, 0): + input("Press [enter] to return...") + else: + raw_input("Press [enter] to return...") From df0a33e6b61e04e41e6be9d038cefe1ae5ffebd1 Mon Sep 17 00:00:00 2001 From: jaminmc <1310376+jaminmc@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:15:52 -0400 Subject: [PATCH 2/2] Added GUI, enhanced CLI, and made python3 When launching the GenSMBIO.command or .bat, It now checks for python 3, and prompts to install if needed. It also creates a .venv to not cause any problems with the rest of the system. Modenized the code to Python 3. No more python 2. Python 2 was sunseted over 5 years ago. Now wuns in gui by default, but can still use text interface with a --tui, or is tkinter is not installed. like in a linux terminal. --- GenSMBIOS.bat | 195 +++++--------- GenSMBIOS.command | 294 +++++++++----------- GenSMBIOS.py | 614 +++++++++++++++++++++++++++--------------- Scripts/__init__.py | 14 +- Scripts/downloader.py | 274 +++++++++---------- Scripts/plist.py | 609 +++++++++++------------------------------ Scripts/run.py | 145 +++++----- Scripts/utils.py | 146 +++++----- 8 files changed, 1046 insertions(+), 1245 deletions(-) diff --git a/GenSMBIOS.bat b/GenSMBIOS.bat index 691aa9f..d64b327 100644 --- a/GenSMBIOS.bat +++ b/GenSMBIOS.bat @@ -4,25 +4,13 @@ set "thisDir=%~dp0" setlocal enableDelayedExpansion REM Setup initial vars -set "script_name=" +set "script_name=%~n0.py" set /a tried=0 set "toask=yes" set "pause_on_error=yes" -set "py2v=" -set "py2path=" set "py3v=" set "py3path=" set "pypath=" -set "targetpy=3" - -REM use_py3: -REM TRUE = Use if found, use py2 otherwise -REM FALSE = Use py2 -REM FORCE = Use py3 -set "use_py3=TRUE" - -REM We'll parse if the first argument passed is -REM --install-python and if so, we'll just install set "just_installing=FALSE" REM Get the system32 (or equivalent) path @@ -66,19 +54,16 @@ if "%~1" == "--install-python" ( goto checkscript :checkscript -REM Check for our script first -set "looking_for=!script_name!" -if "!script_name!" == "" ( - set "looking_for=%~n0.py or %~n0.command" - set "script_name=%~n0.py" - if not exist "!thisDir!\!script_name!" ( - set "script_name=%~n0.command" - ) -) +REM Check for our script if not exist "!thisDir!\!script_name!" ( - echo Could not find !looking_for!. + cls + echo ### ### + echo # Script Not Found # + echo ### ### + echo. + echo Could not find !script_name!. echo Please make sure to run this script from the same directory - echo as !looking_for!. + echo as !script_name!. echo. echo Press [enter] to quit. pause > nul @@ -88,58 +73,35 @@ goto checkpy :checkpy call :updatepath -for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) -for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py2v" "py2path" "py3v" "py3path" ) -for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe py 2^> nul`) do ( call :checkpylauncher "%%x" "py2v" "py2path" "py3v" "py3path" ) -REM Walk our returns to see if we need to install -if /i "!use_py3!" == "FALSE" ( - set "targetpy=2" - set "pypath=!py2path!" -) else if /i "!use_py3!" == "FORCE" ( - set "pypath=!py3path!" -) else if /i "!use_py3!" == "TRUE" ( +for /f "USEBACKQ tokens=*" %%x in (`!syspath!where.exe python3 2^> nul`) do ( call :checkpyversion "%%x" "py3v" "py3path" ) +if not "!py3path!" == "" ( set "pypath=!py3path!" - if "!pypath!" == "" set "pypath=!py2path!" -) -if not "!pypath!" == "" ( - goto runscript + goto setupvenv ) if !tried! lss 1 ( if /i "!toask!"=="yes" ( - REM Better ask permission first goto askinstall ) else ( goto installpy ) ) else ( cls - echo ### ### - echo # Warning # - echo ### ### + echo ### ### + echo # Python Not Found # + echo ### ### echo. - REM Couldn't install for whatever reason - give the error message - echo Python is not installed or not found in your PATH var. + echo Python 3 is not installed or not found in your PATH. echo Please install it from https://www.python.org/downloads/windows/ echo. echo Make sure you check the box labeled: - echo. - echo "Add Python X.X to PATH" - echo. - echo Where X.X is the py version you're installing. + echo "Add Python 3.X to PATH" echo. echo Press [enter] to quit. pause > nul exit /b 1 ) -goto runscript -:checkpylauncher -REM Attempt to check the latest python 2 and 3 versions via the py launcher -for /f "USEBACKQ tokens=*" %%x in (`%~1 -2 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) -for /f "USEBACKQ tokens=*" %%x in (`%~1 -3 -c "import sys; print(sys.executable)" 2^> nul`) do ( call :checkpyversion "%%x" "%~2" "%~3" "%~4" "%~5" ) -goto :EOF - -:checkpyversion +:checkpyversion set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) do ( REM Ensure we have a version number call :isnumber "%%a" @@ -147,20 +109,13 @@ set "version="&for /f "tokens=2* USEBACKQ delims= " %%a in (`"%~1" -V 2^>^&1`) d set "version=%%a" ) if not defined version goto :EOF -if "!version:~0,1!" == "2" ( - REM Python 2 +if "!version:~0,1!" == "3" ( + REM Python 3 call :comparepyversion "!version!" "!%~2!" if "!errorlevel!" == "1" ( set "%~2=!version!" set "%~3=%~1" ) -) else ( - REM Python 3 - call :comparepyversion "!version!" "!%~4!" - if "!errorlevel!" == "1" ( - set "%~4=!version!" - set "%~5=%~1" - ) ) goto :EOF @@ -169,7 +124,7 @@ set "var="&for /f "delims=0123456789." %%i in ("%~1") do set var=%%i if defined var (exit /b 1) exit /b 0 -:comparepyversion +:comparepyversion REM Exits with status 0 if equal, 1 if v1 gtr v2, 2 if v1 lss v2 for /f "tokens=1,2,3 delims=." %%a in ("%~1") do ( set a1=%%a @@ -201,23 +156,18 @@ echo ### ### echo # Python Not Found # echo ### ### echo. -echo Python !targetpy! was not found on the system or in the PATH var. +echo Python 3 was not found on the system or in the PATH. echo. set /p "menu=Would you like to install it now? [y/n]: " if /i "!menu!"=="y" ( - REM We got the OK - install it goto installpy ) else if "!menu!"=="n" ( - REM No OK here... set /a tried=!tried!+1 goto checkpy ) -REM Incorrect answer - go back goto askinstall :installpy -REM This will attempt to download and install python -REM First we get the html for the python downloads page for Windows set /a tried=!tried!+1 cls echo ### ### @@ -226,8 +176,6 @@ echo ### ### echo. echo Gathering info from https://www.python.org/downloads/windows/... powershell -command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12;(new-object System.Net.WebClient).DownloadFile('https://www.python.org/downloads/windows/','%TEMP%\pyurl.txt')" -REM Extract it if it's gzip compressed -powershell -command "$infile='%TEMP%\pyurl.txt';$outfile='%TEMP%\pyurl.temp';try{$input=New-Object System.IO.FileStream $infile,([IO.FileMode]::Open),([IO.FileAccess]::Read),([IO.FileShare]::Read);$output=New-Object System.IO.FileStream $outfile,([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::None);$gzipStream=New-Object System.IO.Compression.GzipStream $input,([IO.Compression.CompressionMode]::Decompress);$buffer=New-Object byte[](1024);while($true){$read=$gzipstream.Read($buffer,0,1024);if($read -le 0){break};$output.Write($buffer,0,$read)};$gzipStream.Close();$output.Close();$input.Close();Move-Item -Path $outfile -Destination $infile -Force}catch{}" if not exist "%TEMP%\pyurl.txt" ( if /i "!just_installing!" == "TRUE" ( echo Failed to get info @@ -238,8 +186,7 @@ if not exist "%TEMP%\pyurl.txt" ( ) echo Parsing for latest... pushd "%TEMP%" -:: Version detection code slimmed by LussacZheng (https://github.com/corpnewt/gibMacOS/issues/20) -for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python !targetpy! Release" pyurl.txt') do ( set "release=%%x" ) +for /f "tokens=9 delims=< " %%x in ('findstr /i /c:"Latest Python 3 Release" pyurl.txt') do ( set "release=%%x" ) popd if "!release!" == "" ( if /i "!just_installing!" == "TRUE" ( @@ -250,19 +197,10 @@ if "!release!" == "" ( ) ) echo Found Python !release! - Downloading... -REM Let's delete our txt file now - we no longer need it del "%TEMP%\pyurl.txt" -REM At this point - we should have the version number. -REM We can build the url like so: "https://www.python.org/ftp/python/[version]/python-[version]-amd64.exe" set "url=https://www.python.org/ftp/python/!release!/python-!release!-amd64.exe" set "pytype=exe" -if "!targetpy!" == "2" ( - set "url=https://www.python.org/ftp/python/!release!/python-!release!.amd64.msi" - set "pytype=msi" -) -REM Now we download it with our slick powershell command powershell -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (new-object System.Net.WebClient).DownloadFile('!url!','%TEMP%\pyinstall.!pytype!')" -REM If it doesn't exist - we bail if not exist "%TEMP%\pyinstall.!pytype!" ( if /i "!just_installing!" == "TRUE" ( echo Failed to download installer @@ -271,42 +209,74 @@ if not exist "%TEMP%\pyinstall.!pytype!" ( goto checkpy ) ) -REM It should exist at this point - let's run it to install silently echo Installing... pushd "%TEMP%" -if /i "!pytype!" == "exe" ( - echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 - pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 -) else ( - set "foldername=!release:.=!" - echo msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" - msiexec /i pyinstall.msi /qb ADDLOCAL=ALL TARGETDIR="%LocalAppData%\Programs\Python\Python!foldername:~0,2!" -) +echo pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 +pyinstall.exe /quiet PrependPath=1 Include_test=0 Shortcuts=0 Include_launcher=0 popd echo Installer finished with %ERRORLEVEL% status. -REM Now we should be able to delete the installer and check for py again del "%TEMP%\pyinstall.!pytype!" -REM If it worked, then we should have python in our PATH -REM this does not get updated right away though - let's try -REM manually updating the local PATH var call :updatepath if /i "!just_installing!" == "TRUE" ( echo. echo Done. + pause > nul + exit /b 0 ) else ( goto checkpy ) -exit /b + +:setupvenv +REM Create virtual environment if it doesn't exist +set "venv_dir=!thisDir!.venv" +if not exist "!venv_dir!\Scripts\python.exe" ( + echo Creating virtual environment... + "!pypath!" -m venv "!venv_dir!" + if not !ERRORLEVEL! == 0 ( + echo Failed to create virtual environment. Falling back to base Python. + set "venv_python=!pypath!" + ) else ( + set "venv_python=!venv_dir!\Scripts\python.exe" + ) +) else ( + set "venv_python=!venv_dir!\Scripts\python.exe" +) +REM Upgrade pip in venv +"!venv_python!" -m pip install --upgrade pip >nul 2>&1 +REM Check and install required modules in venv +echo Checking Python modules... +"!venv_python!" -c "import tqdm" >nul 2>&1 +if not !ERRORLEVEL! == 0 ( + echo tqdm not found. Attempting to install in venv... + "!venv_python!" -m pip install tqdm >nul 2>&1 + if not !ERRORLEVEL! == 0 ( + echo Failed to install tqdm. Continuing without it... + ) +) +"!venv_python!" -c "import requests" >nul 2>&1 +if not !ERRORLEVEL! == 0 ( + echo requests not found. Attempting to install in venv... + "!venv_python!" -m pip install requests >nul 2>&1 + if not !ERRORLEVEL! == 0 ( + echo Failed to install requests. The script may not run without it... + ) +) +"!venv_python!" -c "import tkinter" >nul 2>&1 +if not !ERRORLEVEL! == 0 ( + echo tkinter module not found. + echo Python on Windows typically includes tkinter. Continuing without it... +) +goto runscript :runscript -REM Python found +REM Run the Python script cls set "args=%*" set "args=!args:"=!" if "!args!"=="" ( - "!pypath!" "!thisDir!!script_name!" + "!venv_python!" "!thisDir!!script_name!" ) else ( - "!pypath!" "!thisDir!!script_name!" %* + "!venv_python!" "!thisDir!!script_name!" %* ) if /i "!pause_on_error!" == "yes" ( if not "%ERRORLEVEL%" == "0" ( @@ -337,51 +307,34 @@ set "upath=" for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKCU\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "upath=%%j" ) for /f "USEBACKQ tokens=2* delims= " %%i in (`!syspath!reg.exe query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v "Path" 2^> nul`) do ( if not "%%j" == "" set "spath=%%j" ) if not "%spath%" == "" ( - REM We got something in the system path set "PATH=%spath%" if not "%upath%" == "" ( - REM We also have something in the user path set "PATH=%PATH%;%upath%" ) ) else if not "%upath%" == "" ( set "PATH=%upath%" ) -REM Remove double semicolons from the adjusted PATH call :undouble "PATH" "%PATH%" ";" goto :EOF :getsyspath -REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe by -REM walking the ComSpec var - will also repair it in memory if need be -REM Strip double semi-colons +REM Helper method to return a valid path to cmd.exe, reg.exe, and where.exe call :undouble "temppath" "%ComSpec%" ";" - -REM Dirty hack to leverage the "line feed" approach - there are some odd side -REM effects with this. Do not use this variable name in comments near this -REM line - as it seems to behave erradically. (set LF=^ %=this line is empty=% ) -REM Replace instances of semi-colons with a line feed and wrap -REM in parenthesis to work around some strange batch behavior set "testpath=%temppath:;=!LF!%" - -REM Let's walk each path and test if cmd.exe, reg.exe, and where.exe exist there set /a found=0 for /f "tokens=* delims=" %%i in ("!testpath!") do ( - REM Only continue if we haven't found it yet if not "%%i" == "" ( if !found! lss 1 ( set "checkpath=%%i" - REM Remove "cmd.exe" from the end if it exists if /i "!checkpath:~-7!" == "cmd.exe" ( set "checkpath=!checkpath:~0,-7!" ) - REM Pad the end with a backslash if needed if not "!checkpath:~-1!" == "\" ( set "checkpath=!checkpath!\" ) - REM Let's see if cmd, reg, and where exist there - and set it if so if EXIST "!checkpath!cmd.exe" ( if EXIST "!checkpath!reg.exe" ( if EXIST "!checkpath!where.exe" ( @@ -394,4 +347,4 @@ for /f "tokens=* delims=" %%i in ("!testpath!") do ( ) ) ) -goto :EOF +goto :EOF \ No newline at end of file diff --git a/GenSMBIOS.command b/GenSMBIOS.command index 09927e7..ea1986a 100755 --- a/GenSMBIOS.command +++ b/GenSMBIOS.command @@ -1,77 +1,61 @@ #!/usr/bin/env bash -# Get the curent directory, the script name -# and the script name with "py" substituted for the extension. +# Get the current directory, script name, and target Python script args=( "$@" ) dir="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" script="${0##*/}" target="${script%.*}.py" -# use_py3: -# TRUE = Use if found, use py2 otherwise -# FALSE = Use py2 -# FORCE = Use py3 -use_py3="TRUE" - -# We'll parse if the first argument passed is -# --install-python and if so, we'll just install -just_installing="FALSE" - tempdir="" +kernel="$(uname -s)" +downloaded="FALSE" +just_installing="FALSE" compare_to_version () { - # Compares our OS version to the passed OS version, and - # return a 1 if we match the passed compare type, or a 0 if we don't. + # Compare macOS version to the passed version # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) # $2 = OS version to compare ours to if [ -z "$1" ] || [ -z "$2" ]; then - # Missing info - bail. return fi - local current_os= comp= - current_os="$(sw_vers -productVersion)" + local current_os comp + current_os="$(sw_vers -productVersion 2>/dev/null || echo "0.0.0")" comp="$(vercomp "$current_os" "$2")" - # Check gequal and lequal first - if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || [[ "$comp" == "$1" ]]; then - # Matched + if [[ "$1" == "3" && ("$comp" == "1" || "$comp" == "0") ]] || \ + [[ "$1" == "4" && ("$comp" == "2" || "$comp" == "0") ]] || \ + [[ "$comp" == "$1" ]]; then echo "1" else - # No match echo "0" fi } -set_use_py3_if () { - # Auto sets the "use_py3" variable based on - # conditions passed - # $1 = 0 (equal), 1 (greater), 2 (less), 3 (gequal), 4 (lequal) - # $2 = OS version to compare - # $3 = TRUE/FALSE/FORCE in case of match - if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then - # Missing vars - bail with no changes. - return - fi - if [ "$(compare_to_version "$1" "$2")" == "1" ]; then - use_py3="$3" +format_version () { + local vers="$1" + echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" +} + +vercomp () { + local ver1="$(format_version "$1")" ver2="$(format_version "$2")" + if [ "$ver1" -gt "$ver2" ]; then + echo "1" + elif [ "$ver1" -lt "$ver2" ]; then + echo "2" + else + echo "0" fi } get_remote_py_version () { - local pyurl= py_html= py_vers= py_num="3" + local pyurl py_html py_vers pyurl="https://www.python.org/downloads/macos/" - py_html="$(curl -L $pyurl --compressed 2>&1)" - if [ -z "$use_py3" ]; then - use_py3="TRUE" - fi - if [ "$use_py3" == "FALSE" ]; then - py_num="2" - fi - py_vers="$(echo "$py_html" | grep -i "Latest Python $py_num Release" | awk '{print $8}' | cut -d'<' -f1)" + py_html="$(curl -sL "$pyurl" --compressed 2>/dev/null)" + py_vers="$(echo "$py_html" | grep -i "Latest Python 3 Release" | awk '{print $8}' | cut -d'<' -f1)" echo "$py_vers" } download_py () { - local vers="$1" url= + local vers="$1" url clear echo " ### ###" echo " # Downloading Python #" @@ -82,66 +66,52 @@ download_py () { vers="$(get_remote_py_version)" fi if [ -z "$vers" ]; then - # Didn't get it still - bail print_error fi echo "Located Version: $vers" echo echo "Building download url..." - url="$(curl -L https://www.python.org/downloads/release/python-${vers//./}/ --compressed 2>&1 | grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" + url="$(curl -sL "https://www.python.org/downloads/release/python-${vers//./}/" --compressed 2>/dev/null | \ + grep -iE "python-$vers-macos.*.pkg\"" | awk -F'"' '{ print $2 }')" if [ -z "$url" ]; then - # Couldn't get the URL - bail print_error fi echo " - $url" echo echo "Downloading..." echo - # Create a temp dir and download to it tempdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'tempdir')" - curl "$url" -o "$tempdir/python.pkg" + curl -sL "$url" -o "$tempdir/python.pkg" if [ "$?" != "0" ]; then - echo echo " - Failed to download python installer!" - echo - exit $? + exit 1 fi - echo echo "Running python install package..." echo sudo installer -pkg "$tempdir/python.pkg" -target / if [ "$?" != "0" ]; then - echo echo " - Failed to install python!" - echo - exit $? + exit 1 fi - # Now we expand the package and look for a shell update script - pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" + pkgutil --expand "$tempdir/python.pkg" "$tempdir/python" 2>/dev/null if [ -e "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" ]; then - # Run the script - echo echo "Updating PATH..." echo "$tempdir/python/Python_Shell_Profile_Updater.pkg/Scripts/postinstall" fi vers_folder="Python $(echo "$vers" | cut -d'.' -f1 -f2)" if [ -f "/Applications/$vers_folder/Install Certificates.command" ]; then - # Certs script exists - let's execute that to make sure our certificates are updated - echo echo "Updating Certificates..." echo "/Applications/$vers_folder/Install Certificates.command" fi - echo echo "Cleaning up..." cleanup echo if [ "$just_installing" == "TRUE" ]; then echo "Done." else - # Now we check for py again - echo "Rechecking py..." + echo "Rechecking Python..." downloaded="TRUE" clear main @@ -150,7 +120,7 @@ download_py () { cleanup () { if [ -d "$tempdir" ]; then - rm -Rf "$tempdir" + rm -rf "$tempdir" fi } @@ -161,14 +131,13 @@ print_error() { echo " # Python Not Found #" echo "### ###" echo - echo "Python is not installed or not found in your PATH var." + echo "Python 3 is not installed or not found in your PATH." echo if [ "$kernel" == "Darwin" ]; then echo "Please go to https://www.python.org/downloads/macos/ to" echo "download and install the latest version, then try again." else - echo "Please install python through your package manager and" - echo "try again." + echo "Please install python3 through your package manager and try again." fi echo exit 1 @@ -186,73 +155,29 @@ print_target_missing() { exit 1 } -format_version () { - local vers="$1" - echo "$(echo "$1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }')" -} - -vercomp () { - # Modified from: https://apple.stackexchange.com/a/123408/11374 - local ver1="$(format_version "$1")" ver2="$(format_version "$2")" - if [ $ver1 -gt $ver2 ]; then - echo "1" - elif [ $ver1 -lt $ver2 ]; then - echo "2" - else - echo "0" - fi -} - get_local_python_version() { - # $1 = Python bin name (defaults to python3) - # Echoes the path to the highest version of the passed python bin if any - local py_name="$1" max_version= python= python_version= python_path= - if [ -z "$py_name" ]; then - py_name="python3" + # Find the active python3 in PATH + local python + python="$(command -v python3 2>/dev/null)" + if [ -z "$python" ]; then + return fi - py_list="$(which -a "$py_name" 2>/dev/null)" - # Walk that newline separated list - while read python; do - if [ -z "$python" ]; then - # Got a blank line - skip - continue - fi - if [ "$check_py3_stub" == "1" ] && [ "$python" == "/usr/bin/python3" ]; then - # See if we have a valid developer path - xcode-select -p > /dev/null 2>&1 - if [ "$?" != "0" ]; then - # /usr/bin/python3 path - but no valid developer dir - continue - fi - fi - python_version="$(get_python_version $python)" - if [ -z "$python_version" ]; then - # Didn't find a py version - skip - continue + # Check if it's the macOS stub + if [ "$kernel" == "Darwin" ] && [ "$python" == "/usr/bin/python3" ] && \ + [ "$(compare_to_version "3" "10.15")" == "1" ]; then + xcode-select -p >/dev/null 2>&1 + if [ "$?" != "0" ]; then + return fi - # Got the py version - compare to our max - if [ -z "$max_version" ] || [ "$(vercomp "$python_version" "$max_version")" == "1" ]; then - # Max not set, or less than the current - update it - max_version="$python_version" - python_path="$python" - fi - done <<< "$py_list" - echo "$python_path" -} - -get_python_version() { - local py_path="$1" py_version= - # Get the python version by piping stderr into stdout (for py2), then grepping the output for - # the word "python", getting the second element, and grepping for an alphanumeric version number - py_version="$($py_path -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[A-Za-z\d\.]+")" - if [ ! -z "$py_version" ]; then - echo "$py_version" + fi + python_version="$("$python" -V 2>&1 | grep -i python | cut -d' ' -f2 | grep -E "[0-9]+\.[0-9]+\.[0-9]+")" + if [ -n "$python_version" ]; then + echo "$python" fi } prompt_and_download() { - if [ "$downloaded" != "FALSE" ] || [ "$kernel" != "Darwin" ]; then - # We already tried to download, or we're not on macOS - just bail + if [ "$downloaded" == "TRUE" ] || [ "$kernel" != "Darwin" ]; then print_error fi clear @@ -260,68 +185,95 @@ prompt_and_download() { echo " # Python Not Found #" echo "### ###" echo - target_py="Python 3" - printed_py="Python 2 or 3" - if [ "$use_py3" == "FORCE" ]; then - printed_py="Python 3" - elif [ "$use_py3" == "FALSE" ]; then - target_py="Python 2" - printed_py="Python 2" - fi - echo "Could not locate $printed_py!" + echo "Could not locate Python 3!" echo - echo "This script requires $printed_py to run." + echo "This script requires Python 3 to run." echo while true; do - read -p "Would you like to install the latest $target_py now? (y/n): " yn + read -p "Would you like to install the latest Python 3 now? (y/n): " yn case $yn in - [Yy]* ) download_py;break;; + [Yy]* ) download_py; break;; [Nn]* ) print_error;; esac done } main() { - local python= version= - # Verify our target exists + # Verify target Python script exists if [ ! -f "$dir/$target" ]; then - # Doesn't exist print_target_missing fi - if [ -z "$use_py3" ]; then - use_py3="TRUE" + # Check for base Python 3 + local base_python + base_python="$(get_local_python_version)" + if [ -z "$base_python" ]; then + prompt_and_download + return 1 fi - if [ "$use_py3" != "FALSE" ]; then - # Check for py3 first - python="$(get_local_python_version python3)" + # Create venv in the directory if not exists + local venv_dir="$dir/.venv" + if [ ! -d "$venv_dir" ]; then + echo "Creating virtual environment..." + "$base_python" -m venv "$venv_dir" + if [ $? -ne 0 ]; then + echo "Failed to create virtual environment. Falling back to base Python." + venv_python="$base_python" + else + venv_python="$venv_dir/bin/python" + fi + else + venv_python="$venv_dir/bin/python" fi - if [ "$use_py3" != "FORCE" ] && [ -z "$python" ]; then - # We aren't using py3 explicitly, and we don't already have a path - python="$(get_local_python_version python2)" - if [ -z "$python" ]; then - # Try just looking for "python" - python="$(get_local_python_version python)" + # Upgrade pip in venv + "$venv_python" -m pip install --upgrade pip >/dev/null 2>&1 + # Check and install required modules in venv + echo "Checking Python modules..." + "$venv_python" -c "import tqdm" 2>/dev/null + if [ $? -ne 0 ]; then + echo "tqdm not found. Attempting to install in venv..." + "$venv_python" -m pip install tqdm 2>/dev/null + if [ $? -ne 0 ]; then + echo "Failed to install tqdm. Continuing without it..." fi fi - if [ -z "$python" ]; then - # Didn't ever find it - prompt - prompt_and_download - return 1 + "$venv_python" -c "import requests" 2>/dev/null + if [ $? -ne 0 ]; then + echo "requests not found. Attempting to install in venv..." + "$venv_python" -m pip install requests 2>/dev/null + if [ $? -ne 0 ]; then + echo "Failed to install requests. The script may not run without it." + fi + fi + # Check tkinter + "$venv_python" -c "import tkinter" 2>/dev/null + if [ $? -ne 0 ]; then + echo "tkinter module not found." + if [ -n "$CONDA_PREFIX" ]; then + echo "Detected Conda environment. Attempting to install tk..." + conda install -y tk >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Failed to install tk via conda. Continuing without tkinter..." + else + # Recheck after install + "$venv_python" -c "import tkinter" 2>/dev/null + if [ $? -ne 0 ]; then + echo "tkinter still not available. Continuing without it..." + fi + fi + elif [ "$kernel" == "Darwin" ]; then + echo "The official Python installer includes tkinter. Attempting reinstall..." + download_py + # After reinstall, main is called again, so venv will be checked/created with potentially new base + else + echo "Please install the tkinter package for Python 3 on your system (e.g., sudo apt install python3-tk on Ubuntu)." + echo "Continuing without tkinter..." + fi fi - # Found it - start our script and pass all args - "$python" "$dir/$target" "${args[@]}" + # Run the target script in venv + "$venv_python" "$dir/$target" "${args[@]}" } -# Keep track of whether or not we're on macOS to determine if -# we can download and install python for the user as needed. -kernel="$(uname -s)" -# Check to see if we need to force based on -# macOS version. 10.15 has a dummy python3 version -# that can trip up some py3 detection in other scripts. -# set_use_py3_if "3" "10.15" "FORCE" -downloaded="FALSE" -# Check for the aforementioned /usr/bin/python3 stub if -# our OS version is 10.15 or greater. +# Check for macOS stub check_py3_stub="$(compare_to_version "3" "10.15")" trap cleanup EXIT if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then @@ -329,4 +281,4 @@ if [ "$1" == "--install-python" ] && [ "$kernel" == "Darwin" ]; then download_py else main -fi +fi \ No newline at end of file diff --git a/GenSMBIOS.py b/GenSMBIOS.py index 9b80570..103ffe9 100755 --- a/GenSMBIOS.py +++ b/GenSMBIOS.py @@ -1,28 +1,34 @@ #!/usr/bin/env python -import os, subprocess, shlex, sys, tempfile, shutil, random, uuid, zipfile, json, binascii -from Scripts import downloader, plist, run, utils +import os +import subprocess +import shlex +import sys +import tempfile +import shutil +import uuid +import zipfile +import json +import binascii +import requests from collections import OrderedDict +import plistlib as plist +from secrets import randbits, choice -# Import from secrets - or fall back on random.SystemRandom() -# functions if on python 2 try: - from secrets import randbits, choice - - basestring = str + import tkinter as tk + from tkinter import filedialog, messagebox except ImportError: - from random import SystemRandom + tk = None - _sysrand = SystemRandom() - randbits = _sysrand.getrandbits - choice = _sysrand.choice +try: + from tqdm import tqdm +except ImportError: + tqdm = None class Smbios: def __init__(self): - os.chdir(os.path.dirname(os.path.realpath(__file__))) - self.u = utils.Utils("GenSMBIOS") - self.d = downloader.Downloader() - self.r = run.Run() + os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) self.oc_release_url = ( "https://github.com/acidanthera/OpenCorePkg/releases/latest" ) @@ -40,43 +46,35 @@ def __init__(self): "Memory", ] try: - self.rom_prefixes = json.load( - open(os.path.join(self.scripts, "prefix.json")) - ) + with open(os.path.join(self.scripts, "prefix.json")) as f: + self.rom_prefixes = json.load(f) except: self.rom_prefixes = [] self.settings_file = os.path.join(self.scripts, "settings.json") try: - self.settings = json.load(open(self.settings_file)) + with open(self.settings_file) as f: + self.settings = json.load(f) except: self.settings = {} self.gen_rom = True def _save_settings(self): if self.settings: - try: - json.dump(self.settings, open(self.settings_file, "w"), indent=2) - except: - pass + with open(self.settings_file, "w") as f: + json.dump(self.settings, f, indent=2) elif os.path.exists(self.settings_file): - try: - os.remove(self.settings_file) - except: - pass + os.remove(self.settings_file) def _get_macserial_version(self): - # Attempts to determine the macserial version from the latest OpenCorePkg macserial_v = None try: - urlsource = self.d.get_string(self.oc_release_url, False) + urlsource = requests.get(self.oc_release_url).text for line in urlsource.split("\n"): if "expanded_assets" in line: # Get the version from the URL oc_vers = line.split(' src="')[1].split('"')[0].split("/")[-1] - macserial_h_url = "https://raw.githubusercontent.com/acidanthera/OpenCorePkg/{}/Utilities/macserial/macserial.h".format( - oc_vers - ) - macserial_h = self.d.get_string(macserial_h_url, False) + macserial_h_url = f"https://raw.githubusercontent.com/acidanthera/OpenCorePkg/{oc_vers}/Utilities/macserial/macserial.h" + macserial_h = requests.get(macserial_h_url).text macserial_v = macserial_h.split('#define PROGRAM_VERSION "')[ 1 ].split('"')[0] @@ -87,20 +85,21 @@ def _get_macserial_version(self): def _get_macserial_url(self): # Gets a URL to the latest release of OpenCorePkg try: - urlsource = self.d.get_string(self.oc_release_url, False) + urlsource = requests.get(self.oc_release_url).text for line in urlsource.split("\n"): if "expanded_assets" in line: - expanded_html = self.d.get_string( - line.split(' src="')[1].split('"')[0], False - ) + expanded_html = requests.get( + line.split(' src="')[1].split('"')[0] + ).text for l in expanded_html.split("\n"): if ( 'href="/acidanthera/OpenCorePkg/releases/download/' in l and "-RELEASE.zip" in l ): # Got it - return "https://github.com{}".format( - l.split('href="')[1].split('"')[0] + return ( + "https://github.com" + + l.split('href="')[1].split('"')[0] ) except: pass @@ -108,18 +107,14 @@ def _get_macserial_url(self): def _get_binary(self, binary_name=None): if not binary_name: - binary_name = ( - ["macserial.exe", "macserial32.exe"] - if os.name == "nt" - else ( - ["macserial.linux", "macserial"] - if sys.platform.startswith("linux") - else ["macserial"] - ) - ) - # Check locally + if os.name == "nt": + binary_name = ["macserial.exe", "macserial32.exe"] + elif sys.platform.startswith("linux"): + binary_name = ["macserial.linux", "macserial"] + else: + binary_name = ["macserial"] cwd = os.getcwd() - os.chdir(os.path.dirname(os.path.realpath(__file__))) + os.chdir(os.path.dirname(os.path.realpath(sys.argv[0]))) path = None for name in binary_name: if os.path.exists(name): @@ -133,21 +128,15 @@ def _get_binary(self, binary_name=None): def _get_version(self, macserial): # Gets the macserial version - out, error, code = self.r.run({"args": [macserial]}) - if not len(out): + p = subprocess.run([macserial], capture_output=True, text=True) + out = p.stdout + if not out: return None for line in out.split("\n"): if not line.lower().startswith("version"): continue - vers = next( - ( - x - for x in line.lower().strip().split() - if len(x) and x[0] in "0123456789" - ), - None, - ) - if not vers is None and vers[-1] == ".": + vers = next((x for x in line.strip().split() if x and x[0].isdigit()), None) + if vers and vers[-1] == ".": vers = vers[:-1] return vers return None @@ -155,45 +144,54 @@ def _get_version(self, macserial): def _download_and_extract(self, temp, url, path_in_zip=[]): ztemp = tempfile.mkdtemp(dir=temp) zfile = os.path.basename(url) - print("\nDownloading {}...".format(os.path.basename(url))) - result = self.d.stream_to_file(url, os.path.join(ztemp, zfile)) - print("") - if not result: + print("\nDownloading {}...".format(zfile)) + r = requests.get(url, stream=True) + if not r.ok: raise Exception(" - Failed to download!") + with open(os.path.join(ztemp, zfile), "wb") as f: + total = int(r.headers.get("content-length", 0)) + if tqdm and total: + with tqdm(total=total, unit="B", unit_scale=True) as pbar: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + pbar.update(len(chunk)) + else: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + print("") print(" - Extracting...") btemp = tempfile.mkdtemp(dir=temp) - # Extract with built-in tools \o/ with zipfile.ZipFile(os.path.join(ztemp, zfile)) as z: - z.extractall(os.path.join(temp, btemp)) + z.extractall(btemp) script_dir = os.path.join( - os.path.dirname(os.path.realpath(__file__)), self.scripts + os.path.dirname(os.path.realpath(sys.argv[0])), self.scripts ) - search_path = os.path.join(temp, btemp) - # Extend the search path if path_in_zip contains elements + search_path = btemp if path_in_zip: search_path = os.path.join(search_path, *path_in_zip) for x in os.listdir(search_path): if "macserial" in x.lower(): # Found one print(" - Found {}".format(x)) + full_path = os.path.join(search_path, x) if os.name != "nt": print(" - Chmod +x...") - self.r.run({"args": ["chmod", "+x", os.path.join(search_path, x)]}) + os.chmod(full_path, 0o755) print(" - Copying to {} directory...".format(self.scripts)) if not os.path.exists(script_dir): - os.mkdir(script_dir) - shutil.copy(os.path.join(search_path, x), os.path.join(script_dir, x)) + os.makedirs(script_dir, exist_ok=True) + shutil.copy(full_path, os.path.join(script_dir, x)) - def _get_macserial(self): - # Download both the windows and mac versions of macserial and expand them to the Scripts dir - self.u.head("Getting MacSerial") - print("") + def _get_macserial(self, from_gui=False): print("Gathering latest macserial info...") url = self._get_macserial_url() path_in_zip = ["Utilities", "macserial"] if not url: print("Error checking for updates (network issue)\n") - self.u.grab("Press [enter] to return...") + if not from_gui: + input("Press [enter] to return...") return temp = tempfile.mkdtemp() cwd = os.getcwd() @@ -205,24 +203,20 @@ def _get_macserial(self): print("\nCleaning up...") os.chdir(cwd) shutil.rmtree(temp) - self.u.grab("\nDone.", timeout=5) - return + if not from_gui: + input("\nDone. Press [enter] to return...") def _get_remote_version(self): - self.u.head("Getting MacSerial Remote Version") - print("") print("Gathering latest macserial info...") print(" - Gathering info from OpenCorePkg...") vers = self._get_macserial_version() if not vers: print("Error checking for updates (network issue)\n") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return None return vers def _get_plist(self): - self.u.head("Select Plist") - print("") print("Current: {}".format(self.plist)) print("Type: {}".format(self.plist_type)) print("") @@ -230,33 +224,25 @@ def _get_plist(self): print("M. Main Menu") print("Q. Quit") print("") - p = self.u.grab("Please drag and drop the target plist: ") + p = input("Please drag and drop the target plist: ").strip('"').strip("'") if p.lower() == "q": - self.u.custom_quit() + sys.exit(0) elif p.lower() == "m": return elif p.lower() == "c": self.plist = None self.plist_data = None return - - pc = self.u.check_path(p) - if not pc: - self.u.head("File Missing") - print("") + if not os.path.exists(p): print("Plist file not found:\n\n{}".format(p)) - print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return self._get_plist() try: - with open(pc, "rb") as f: + with open(p, "rb") as f: self.plist_data = plist.load(f, dict_type=OrderedDict) except Exception as e: - self.u.head("Plist Malformed") - print("") print("Plist file malformed:\n\n{}".format(e)) - print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return self._get_plist() # Got a valid plist - let's try to check for Clover or OC structure detected_type = ( @@ -264,11 +250,8 @@ def _get_plist(self): if "PlatformInfo" in self.plist_data else "Clover" if "SMBIOS" in self.plist_data else "Unknown" ) - if detected_type.lower() == "unknown": - # Have the user decide which to do + if detected_type == "Unknown": while True: - self.u.head("Unknown Plist Type") - print("") print("Could not auto-determine plist type!") print("") print("1. Clover") @@ -276,7 +259,7 @@ def _get_plist(self): print("") print("M. Return to the Menu") print("") - t = self.u.grab("Please select the target type: ").lower() + t = input("Please select the target type: ").lower() if t == "m": return self._get_plist() elif t in ("1", "2"): @@ -285,7 +268,7 @@ def _get_plist(self): # Got a plist and type - let's save it self.plist_type = detected_type # Apply any key-stripping or safety checks - if self.plist_type.lower() == "clover": + if self.plist_type == "Clover": # Got a valid clover plist - let's check keys key_check = self.plist_data.get("SMBIOS", {}) new_smbios = {} @@ -299,16 +282,14 @@ def _get_plist(self): # We want the SmUUID to be the top-level - remove CustomUUID if exists if "CustomUUID" in self.plist_data.get("SystemParameters", {}): removed_keys.append("CustomUUID") - if len(removed_keys): + if removed_keys: while True: - self.u.head("") - print("") print( "The following keys will be removed:\n\n{}\n".format( ", ".join(removed_keys) ) ) - con = self.u.grab("Continue? (y/n): ") + con = input("Continue? (y/n): ") if con.lower() == "y": # Flush settings self.plist_data["SMBIOS"] = new_smbios @@ -320,7 +301,7 @@ def _get_plist(self): elif con.lower() == "n": self.plist_data = None return - self.plist = pc + self.plist = p def _get_rom(self): # Generate 6-bytes of cryptographically random values @@ -328,22 +309,20 @@ def _get_rom(self): if self.rom_prefixes: # Replace the prefix with one from our list prefix = choice(self.rom_prefixes) - if isinstance(prefix, basestring): - rom_str = prefix + rom_str[len(prefix) :] + rom_str = prefix + rom_str[len(prefix) :] return rom_str def _get_smbios(self, macserial, smbios_type, times=1): # Returns a list of SMBIOS lines that match total = [] # Get any additional args and ensure they're a string - args = self.settings.get("macserial_args") - if not isinstance(args, basestring): - args = "" + args = self.settings.get("macserial_args", "") + args = shlex.split(args) while len(total) < times: total_len = len(total) - smbios, err, code = self.r.run( - {"args": [macserial, "-a"] + shlex.split(args)} - ) + p = subprocess.run([macserial, "-a"] + args, capture_output=True, text=True) + smbios = p.stdout + code = p.returncode if code != 0: # Issues generating return None @@ -377,21 +356,18 @@ def _generate_smbios(self, macserial): macserial = self._get_binary() if not macserial or not os.path.exists(macserial): # Could not find it, and it failed to download :( - self.u.head("Missing MacSerial") - print("") print("MacSerial binary was not found and failed to download.") - print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return - self.u.head("Generate SMBIOS") + print("Generate SMBIOS") print("") print("M. Main Menu") print("Q. Quit") print("") print("Please type the SMBIOS to gen and the number") - menu = self.u.grab("of times to generate [max 20] (i.e. iMac18,3 5): ") + menu = input("of times to generate [max 20] (i.e. iMac18,3 5): ") if menu.lower() == "q": - self.u.custom_quit() + sys.exit(0) elif menu.lower() == "m": return menu = menu.split(" ") @@ -404,33 +380,25 @@ def _generate_smbios(self, macserial): try: times = int(menu[1]) except: - self.u.head("Incorrect Input") - print("") print("Incorrect format - must be SMBIOS times - i.e. iMac18,3 5") - print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") self._generate_smbios(macserial) return # Keep it between 1 and 20 - if times < 1: - times = 1 - if times > 20: - times = 20 + times = max(1, min(20, times)) smbios = self._get_smbios(macserial, smtype, times) if smbios is None: # Issues generating print("Error - macserial returned an error!") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return if smbios == False: print("\nError - {} not generated by macserial\n".format(smtype)) - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return - self.u.head("{} SMBIOS Info".format(smbios[0][0])) + print("{} SMBIOS Info".format(smbios[0][0])) print("") - if self.settings.get("macserial_args") and isinstance( - self.settings["macserial_args"], basestring - ): + if self.settings.get("macserial_args"): print("Additional Arguments Passed:") print(" {}".format(self.settings["macserial_args"])) print("") @@ -448,27 +416,28 @@ def _generate_smbios(self, macserial): print("\nFlushing first SMBIOS entry to {}".format(self.plist)) else: print("\nFlushing SMBIOS entry to {}".format(self.plist)) - if self.plist_type.lower() == "clover": + if self.plist_type == "Clover": # Ensure plist data exists for x in ["SMBIOS", "RtVariables", "SystemParameters"]: - if not x in self.plist_data: - self.plist_data[x] = {} + self.plist_data[x] = self.plist_data.get(x, {}) self.plist_data["SMBIOS"]["ProductName"] = smbios[0][0] self.plist_data["SMBIOS"]["SerialNumber"] = smbios[0][1] self.plist_data["SMBIOS"]["BoardSerialNumber"] = smbios[0][2] self.plist_data["RtVariables"]["MLB"] = smbios[0][2] self.plist_data["SMBIOS"]["SmUUID"] = smbios[0][3] if self.gen_rom: - self.plist_data["RtVariables"]["ROM"] = plist.wrap_data( - binascii.unhexlify(smbios[0][4].encode("utf-8")) + self.plist_data["RtVariables"]["ROM"] = binascii.unhexlify( + smbios[0][4].encode() ) self.plist_data["SystemParameters"]["InjectSystemID"] = True - elif self.plist_type.lower() == "opencore": + elif self.plist_type == "OpenCore": # Ensure data exists - if not "PlatformInfo" in self.plist_data: - self.plist_data["PlatformInfo"] = {} - if not "Generic" in self.plist_data["PlatformInfo"]: - self.plist_data["PlatformInfo"]["Generic"] = {} + self.plist_data["PlatformInfo"] = self.plist_data.get( + "PlatformInfo", {} + ) + self.plist_data["PlatformInfo"]["Generic"] = self.plist_data[ + "PlatformInfo" + ].get("Generic", {}) # Set the values self.plist_data["PlatformInfo"]["Generic"]["SystemProductName"] = ( smbios[0][0] @@ -479,46 +448,37 @@ def _generate_smbios(self, macserial): self.plist_data["PlatformInfo"]["Generic"]["MLB"] = smbios[0][2] self.plist_data["PlatformInfo"]["Generic"]["SystemUUID"] = smbios[0][3] if self.gen_rom: - self.plist_data["PlatformInfo"]["Generic"]["ROM"] = plist.wrap_data( - binascii.unhexlify(smbios[0][4].encode("utf-8")) + self.plist_data["PlatformInfo"]["Generic"]["ROM"] = ( + binascii.unhexlify(smbios[0][4].encode()) ) with open(self.plist, "wb") as f: plist.dump(self.plist_data, f, sort_keys=False) # Got only valid keys now print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") def _list_current(self, macserial): if not macserial or not os.path.exists(macserial): - self.u.head("Missing MacSerial") - print("") print("MacSerial binary not found.") - print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") return - out, err, code = self.r.run({"args": [macserial]}) + p = subprocess.run([macserial], capture_output=True, text=True) + out = p.stdout out = "\n".join( - [ - x - for x in out.split("\n") - if not x.lower().startswith("version") and len(x) - ] + [x for x in out.split("\n") if not x.lower().startswith("version") and x] ) - self.u.head("Current SMBIOS Info") + print("Current SMBIOS Info") print("") print(out) print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") def get_additional_args(self): while True: - self.u.head("Additional Arguments") + print("Additional Arguments") print("") - print("Current Additional Arguments:") args = self.settings.get("macserial_args") - if not args or not isinstance(args, basestring): - args = None - print(" {}".format(args)) + print("Current Additional Arguments: {}".format(args)) print("") print( "The -a argument is always passed to macserial, but you can enter additional" @@ -529,13 +489,13 @@ def get_additional_args(self): print("M. Return To Main Menu") print("Q. Quit") print("") - args = self.u.grab("Please type the arguments to pass: ") - if not len(args): + args = input("Please type the arguments to pass: ") + if not args: continue elif args.lower() == "m": return elif args.lower() == "q": - self.u.custom_quit() + sys.exit(0) elif args.lower() == "c": self.settings.pop("macserial_args", None) self._save_settings() @@ -544,17 +504,15 @@ def get_additional_args(self): self._save_settings() def main(self): - self.u.head() - print("") macserial = self._get_binary() if macserial: - macserial_v = self._get_version(macserial) + macserial_v = self._get_version(macserial) or "Not Found" print("MacSerial v{}".format(macserial_v)) else: macserial_v = "0.0.0" print("MacSerial not found!") # Print remote version if possible - if self.remote and self.u.compare_versions(macserial_v, self.remote): + if self.remote and self._compare_versions(macserial_v, self.remote): print("Remote Version v{}".format(self.remote)) print("Current plist: {}".format(self.plist)) print("Plist type: {}".format(self.plist_type)) @@ -571,17 +529,15 @@ def main(self): ) ) args = self.settings.get("macserial_args") - if not args or not isinstance(args, basestring): - args = None print("8. Additional Args (Currently: {})".format(args)) print("") print("Q. Quit") print("") - menu = self.u.grab("Please select an option: ").lower() - if not len(menu): + menu = input("Please select an option: ").lower() + if not menu: return if menu == "q": - self.u.custom_quit() + sys.exit(0) elif menu == "1": self._get_macserial() elif menu == "2": @@ -589,13 +545,13 @@ def main(self): elif menu == "3": self._generate_smbios(macserial) elif menu == "4": - self.u.head("Generated UUID") + print("Generated UUID") print("") print(str(uuid.uuid4()).upper()) print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") elif menu == "5": - self.u.head("Generated ROM") + print("Generated ROM") print("") print( "{} ROM: {}".format( @@ -603,7 +559,7 @@ def main(self): ) ) print("") - self.u.grab("Press [enter] to return...") + input("Press [enter] to return...") elif menu == "6": self._list_current(macserial) elif menu == "7": @@ -611,6 +567,224 @@ def main(self): elif menu == "8": self.get_additional_args() + def _compare_versions(self, local, remote): + def version_tuple(v): + return tuple(map(int, v.split("."))) + + return version_tuple(local) < version_tuple(remote) + + +class GUI: + def __init__(self, s): + self.s = s + self.root = tk.Tk() + self.root.title("GenSMBIOS") + frame = tk.Frame(self.root) + frame.pack(padx=10, pady=10) + self.plist_label = tk.Label( + frame, text=f"Current plist: {self.s.plist or 'None'} ({self.s.plist_type})" + ) + self.plist_label.pack() + tk.Button(frame, text="Select config.plist", command=self.select_plist).pack() + tk.Label(frame, text="SMBIOS Type (e.g. iMac18,3)").pack() + self.smbios_type = tk.Entry(frame) + self.smbios_type.pack() + tk.Label(frame, text="Count (1-20, default 1)").pack() + self.times = tk.Entry(frame) + self.times.pack() + tk.Button(frame, text="Generate SMBIOS", command=self.generate_smbios).pack() + self.gen_rom_var = tk.BooleanVar(value=self.s.gen_rom) + tk.Checkbutton( + frame, + text="Generate ROM with SMBIOS", + variable=self.gen_rom_var, + command=self.toggle_gen_rom, + ).pack() + tk.Label(frame, text="Additional Args").pack() + self.args_entry = tk.Entry(frame) + self.args_entry.insert(0, self.s.settings.get("macserial_args", "")) + self.args_entry.pack() + tk.Button(frame, text="Set Additional Args", command=self.set_args).pack() + tk.Button(frame, text="Install/Update MacSerial", command=self.install).pack() + tk.Button(frame, text="Generate UUID", command=self.gen_uuid).pack() + tk.Button(frame, text="Generate ROM", command=self.gen_rom_func).pack() + tk.Button(frame, text="List Current SMBIOS", command=self.list_smbios).pack() + self.output = tk.Text(frame, height=15, width=60) + self.output.pack() + self.root.mainloop() + + def toggle_gen_rom(self): + self.s.gen_rom = self.gen_rom_var.get() + + def set_args(self): + args = self.args_entry.get().strip() + if args: + self.s.settings["macserial_args"] = args + else: + self.s.settings.pop("macserial_args", None) + self.s._save_settings() + messagebox.showinfo("Success", "Additional arguments updated.") + + def install(self): + self.s._get_macserial(from_gui=True) + messagebox.showinfo("Success", "MacSerial installed/updated.") + + def gen_uuid(self): + u = str(uuid.uuid4()).upper() + self.output.insert(tk.END, f"Generated UUID: {u}\n\n") + self.output.see(tk.END) + + def gen_rom_func(self): + r = self.s._get_rom() + prefix = "Apple" if self.s.rom_prefixes else "Random" + self.output.insert(tk.END, f"Generated {prefix} ROM: {r}\n\n") + self.output.see(tk.END) + + def list_smbios(self): + macserial = self.s._get_binary() + if not macserial: + messagebox.showerror("Error", "MacSerial not found. Try installing it.") + return + p = subprocess.run([macserial], capture_output=True, text=True) + out = "\n".join( + [ + x + for x in p.stdout.split("\n") + if not x.lower().startswith("version") and x + ] + ) + self.output.insert(tk.END, f"Current SMBIOS Info:\n{out}\n\n") + self.output.see(tk.END) + + def select_plist(self): + path = filedialog.askopenfilename( + title="Select config.plist", + filetypes=(("Plist files", "*.plist"), ("All files", "*.*")), + ) + if not path: + return + try: + with open(path, "rb") as f: + self.s.plist_data = plist.load(f, dict_type=OrderedDict) + detected_type = ( + "OpenCore" + if "PlatformInfo" in self.s.plist_data + else "Clover" if "SMBIOS" in self.s.plist_data else "Unknown" + ) + if detected_type == "Unknown": + if messagebox.askyesno( + "Plist Type", "Is this an OpenCore plist? (Click No for Clover)" + ): + detected_type = "OpenCore" + else: + detected_type = "Clover" + self.s.plist_type = detected_type + if detected_type == "Clover": + key_check = self.s.plist_data.get("SMBIOS", {}) + new_smbios = { + k: v for k, v in key_check.items() if k in self.s.okay_keys + } + removed_keys = [k for k in key_check if k not in self.s.okay_keys] + if "CustomUUID" in self.s.plist_data.get("SystemParameters", {}): + removed_keys.append("CustomUUID") + self.s.plist_data.get("SystemParameters", {}).pop( + "CustomUUID", None + ) + if removed_keys: + if not messagebox.askyesno( + "Confirm", + f"The following keys will be removed:\n{', '.join(removed_keys)}\nContinue?", + ): + self.s.plist_data = None + return + self.s.plist_data["SMBIOS"] = new_smbios + self.s.plist = path + self.plist_label.config( + text=f"Current plist: {self.s.plist} ({self.s.plist_type})" + ) + except Exception as e: + messagebox.showerror("Error", f"Failed to load plist: {e}") + + def generate_smbios(self): + smtype = self.smbios_type.get().strip() + if not smtype: + messagebox.showerror("Error", "Enter SMBIOS type.") + return + times_input = self.times.get().strip() + try: + times = int(times_input) if times_input else 1 + if not 1 <= times <= 20: + raise ValueError + except ValueError: + messagebox.showerror("Error", "Count must be an integer between 1 and 20.") + return + macserial = self.s._get_binary() + if not macserial: + if messagebox.askyesno("Download", "MacSerial not found. Download now?"): + self.s._get_macserial(from_gui=True) + macserial = self.s._get_binary() + if not macserial: + messagebox.showerror("Error", "MacSerial not found.") + return + smbios = self.s._get_smbios(macserial, smtype, times) + if smbios is None: + messagebox.showerror("Error", "macserial returned an error.") + return + if smbios == False: + messagebox.showerror("Error", f"{smtype} not supported by macserial.") + return + out = f"{smbios[0][0]} SMBIOS Info\n" + if "macserial_args" in self.s.settings: + out += f"Additional Arguments Passed: {self.s.settings['macserial_args']}\n" + f_string = ( + "Type: {}\nSerial: {}\nBoard Serial: {}\nSmUUID: {}\n" + ) + if self.s.gen_rom: + f_string += ( + "Apple ROM: {}\n" if self.s.rom_prefixes else "Random ROM: {}\n" + ) + out += "\n".join([f_string.format(*x) for x in smbios]) + if self.s.plist_data and self.s.plist: + if messagebox.askyesno("Apply", "Apply first SMBIOS entry to plist?"): + sm = smbios[0] + if self.s.plist_type == "Clover": + for x in ["SMBIOS", "RtVariables", "SystemParameters"]: + self.s.plist_data[x] = self.s.plist_data.get(x, {}) + self.s.plist_data["SMBIOS"]["ProductName"] = sm[0] + self.s.plist_data["SMBIOS"]["SerialNumber"] = sm[1] + self.s.plist_data["SMBIOS"]["BoardSerialNumber"] = sm[2] + self.s.plist_data["RtVariables"]["MLB"] = sm[2] + self.s.plist_data["SMBIOS"]["SmUUID"] = sm[3] + if self.s.gen_rom: + self.s.plist_data["RtVariables"]["ROM"] = binascii.unhexlify( + sm[4].encode() + ) + self.s.plist_data["SystemParameters"]["InjectSystemID"] = True + elif self.s.plist_type == "OpenCore": + self.s.plist_data["PlatformInfo"] = self.s.plist_data.get( + "PlatformInfo", {} + ) + self.s.plist_data["PlatformInfo"]["Generic"] = self.s.plist_data[ + "PlatformInfo" + ].get("Generic", {}) + self.s.plist_data["PlatformInfo"]["Generic"][ + "SystemProductName" + ] = sm[0] + self.s.plist_data["PlatformInfo"]["Generic"][ + "SystemSerialNumber" + ] = sm[1] + self.s.plist_data["PlatformInfo"]["Generic"]["MLB"] = sm[2] + self.s.plist_data["PlatformInfo"]["Generic"]["SystemUUID"] = sm[3] + if self.s.gen_rom: + self.s.plist_data["PlatformInfo"]["Generic"]["ROM"] = ( + binascii.unhexlify(sm[4].encode()) + ) + with open(self.s.plist, "wb") as f: + plist.dump(self.s.plist_data, f, sort_keys=False) + out += f"\nFlushed to {self.s.plist}" + self.output.insert(tk.END, out + "\n\n") + self.output.see(tk.END) + if __name__ == "__main__": import argparse @@ -619,18 +793,19 @@ def main(self): parser.add_argument( "--install", action="store_true", help="Install/Update MacSerial" ) - parser.add_argument("--plist", type=str, help="Path to config.plist") + parser.add_argument("-p", "--plist", type=str, help="Path to config.plist") parser.add_argument( "--plist-type", choices=["clover", "opencore"], help="Specify plist type if not auto-detected", ) parser.add_argument( + "-g", "--generate", nargs="+", help="Generate SMBIOS: [times] (e.g., iMac18,3 5)", ) - parser.add_argument("--uuid", action="store_true", help="Generate UUID") + parser.add_argument("-u", "--uuid", action="store_true", help="Generate UUID") parser.add_argument("--rom", action="store_true", help="Generate ROM") parser.add_argument("--list", action="store_true", help="List current SMBIOS") parser.add_argument( @@ -644,6 +819,8 @@ def main(self): parser.add_argument( "-j", "--json", type=str, help="Export generated SMBIOS to JSON file" ) + parser.add_argument("--gui", action="store_true", help="Run in GUI mode") + parser.add_argument("--tui", action="store_true", help="Run in text UI mode") args = parser.parse_args() s = Smbios() @@ -654,8 +831,8 @@ def main(self): processed = True if args.plist: - pc = s.u.check_path(args.plist) - if not pc: + pc = args.plist + if not os.path.exists(pc): print("Plist file not found: {}".format(args.plist)) sys.exit(1) try: @@ -690,17 +867,14 @@ def main(self): print("Removed keys from plist: {}".format(", ".join(removed_keys))) s.plist_data["SMBIOS"] = new_smbios s.plist = pc - processed = True if args.clear_args: s.settings.pop("macserial_args", None) s._save_settings() - processed = True if args.args is not None: s.settings["macserial_args"] = args.args s._save_settings() - processed = True if args.toggle_rom: s.gen_rom = not s.gen_rom @@ -709,7 +883,6 @@ def main(self): "Enabled" if s.gen_rom else "Disabled" ) ) - processed = True if args.generate: macserial = s._get_binary() @@ -719,10 +892,7 @@ def main(self): menu = args.generate smtype = menu[0] times = 1 if len(menu) < 2 else int(menu[1]) - if times < 1: - times = 1 - if times > 20: - times = 20 + times = max(1, min(20, times)) smbios = s._get_smbios(macserial, smtype, times) if smbios is None: print("Error - macserial returned an error!") @@ -739,7 +909,9 @@ def main(self): "Type: {}\nSerial: {}\nBoard Serial: {}\nSmUUID: {}" ) if s.gen_rom: - f_string += "\nApple ROM: {}" if s.rom_prefixes else "\nRandom ROM: {}" + f_string += ( + "\nApple ROM: {}" if self.rom_prefixes else "\nRandom ROM: {}" + ) print("\n\n".join([f_string.format(*x) for x in smbios])) if s.plist_data and s.plist: if len(smbios) > 1: @@ -756,8 +928,8 @@ def main(self): s.plist_data["RtVariables"]["MLB"] = smbios[0][2] s.plist_data["SMBIOS"]["SmUUID"] = smbios[0][3] if s.gen_rom: - s.plist_data["RtVariables"]["ROM"] = plist.wrap_data( - binascii.unhexlify(smbios[0][4].encode("utf-8")) + s.plist_data["RtVariables"]["ROM"] = binascii.unhexlify( + smbios[0][4].encode() ) s.plist_data["SystemParameters"]["InjectSystemID"] = True elif s.plist_type == "OpenCore": @@ -774,11 +946,11 @@ def main(self): s.plist_data["PlatformInfo"]["Generic"]["MLB"] = smbios[0][2] s.plist_data["PlatformInfo"]["Generic"]["SystemUUID"] = smbios[0][3] if s.gen_rom: - s.plist_data["PlatformInfo"]["Generic"]["ROM"] = plist.wrap_data( - binascii.unhexlify(smbios[0][4].encode("utf-8")) + s.plist_data["PlatformInfo"]["Generic"]["ROM"] = binascii.unhexlify( + smbios[0][4].encode() ) - with open(s.plist, "wb") as f: - plist.dump(s.plist_data, f, sort_keys=False) + with open(s.plist, "wb") as f: + plist.dump(s.plist_data, f, sort_keys=False) if args.json: data = [] for x in smbios: @@ -805,13 +977,10 @@ def main(self): if not macserial: print("MacSerial not found! Use --install to download.") sys.exit(1) - out, err, code = s.r.run({"args": [macserial]}) + p = subprocess.run([macserial], capture_output=True, text=True) + out = p.stdout out = "\n".join( - [ - x - for x in out.split("\n") - if not x.lower().startswith("version") and len(x) - ] + [x for x in out.split("\n") if not x.lower().startswith("version") and x] ) print("Current SMBIOS Info") print(out) @@ -827,13 +996,20 @@ def main(self): print("Remote Version v{}".format(s.remote)) processed = True - if not processed: - while True: - try: + if args.gui: + if tk is not None: + GUI(s) + else: + print("Tkinter is not available on this system. Falling back to CLI mode.") + while True: s.main() - except Exception as e: - print(e) - if sys.version_info >= (3, 0): - input("Press [enter] to return...") - else: - raw_input("Press [enter] to return...") + else: + if not processed: + if args.tui: + while True: + s.main() + elif tk is not None: + GUI(s) + else: + while True: + s.main() diff --git a/Scripts/__init__.py b/Scripts/__init__.py index 962c1d3..19a49eb 100644 --- a/Scripts/__init__.py +++ b/Scripts/__init__.py @@ -1,4 +1,10 @@ -from os.path import dirname, basename, isfile -import glob -modules = glob.glob(dirname(__file__)+"/*.py") -__all__ = [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')] \ No newline at end of file +from pathlib import Path +from typing import List + +# Get all Python files in the current directory, excluding __init__.py +modules = Path(__file__).parent.glob("*.py") +__all__: List[str] = [ + module.stem + for module in modules + if module.is_file() and module.name != "__init__.py" +] diff --git a/Scripts/downloader.py b/Scripts/downloader.py index c5b3b40..2567150 100755 --- a/Scripts/downloader.py +++ b/Scripts/downloader.py @@ -1,52 +1,74 @@ -import sys, os, time, ssl, gzip, multiprocessing +import sys +import os +import time +import ssl +import gzip +import multiprocessing from io import BytesIO -# Python-aware urllib stuff -try: - from urllib.request import urlopen, Request - import queue as q -except ImportError: - # Import urllib2 to catch errors - import urllib2 - from urllib2 import urlopen, Request - import Queue as q +from urllib.request import urlopen, Request +import queue as q + +TERMINAL_WIDTH = ( + os.get_terminal_size().columns + if hasattr(os, "get_terminal_size") + else (120 if os.name == "nt" else 80) +) -TERMINAL_WIDTH = 120 if os.name=="nt" else 80 def get_size(size, suffix=None, use_1024=False, round_to=2, strip_zeroes=False): # size is the number of bytes # suffix is the target suffix to locate (B, KB, MB, etc) - if found - # use_2014 denotes whether or not we display in MiB vs MB - # round_to is the number of dedimal points to round our result to (0-15) - # strip_zeroes denotes whether we strip out zeroes + # use_1024 denotes whether or not we display in MiB vs MB + # round_to is the number of decimal points to round our result to (0-15) + # strip_zeroes denotes whether we strip out zeroes # Failsafe in case our size is unknown if size == -1: return "Unknown" # Get our suffixes based on use_1024 - ext = ["B","KiB","MiB","GiB","TiB","PiB"] if use_1024 else ["B","KB","MB","GB","TB","PB"] + ext = ( + ["B", "KiB", "MiB", "GiB", "TiB", "PiB"] + if use_1024 + else ["B", "KB", "MB", "GB", "TB", "PB"] + ) div = 1024 if use_1024 else 1000 s = float(size) - s_dict = {} # Initialize our dict - # Iterate the ext list, and divide by 1000 or 1024 each time to setup the dict {ext:val} + s_dict = {} # Initialize our dict + # Iterate the ext, and divide by 1000 or 1024 each time to setup the dict {ext:val} for e in ext: s_dict[e] = s s /= div # Get our suffix if provided - will be set to None if not found, or if started as None - suffix = next((x for x in ext if x.lower() == suffix.lower()),None) if suffix else suffix + suffix = ( + next((x for x in ext if x.lower() == suffix.lower()), None) + if suffix + else suffix + ) # Get the largest value that's still over 1 biggest = suffix if suffix else next((x for x in ext[::-1] if s_dict[x] >= 1), "B") # Determine our rounding approach - first make sure it's an int; default to 2 on error - try:round_to=int(round_to) - except:round_to=2 - round_to = 0 if round_to < 0 else 15 if round_to > 15 else round_to # Ensure it's between 0 and 15 + try: + round_to = int(round_to) + except: + round_to = 2 + round_to = ( + 0 if round_to < 0 else 15 if round_to > 15 else round_to + ) # Ensure it's between 0 and 15 bval = round(s_dict[biggest], round_to) # Split our number based on decimal points - a,b = str(bval).split(".") + a, b = str(bval).split(".") # Check if we need to strip or pad zeroes - b = b.rstrip("0") if strip_zeroes else b.ljust(round_to,"0") if round_to > 0 else "" - return "{:,}{} {}".format(int(a),"" if not b else "."+b,biggest) + b = ( + b.rstrip("0") + if strip_zeroes + else b.ljust(round_to, "0") if round_to > 0 else "" + ) + return "{:,}{} {}".format(int(a), "" if not b else "." + b, biggest) + -def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_packets=0): +def _process_hook( + queue, total_size, bytes_so_far=0, update_interval=1.0, max_packets=0 +): packets = [] speed = remaining = "" last_update = time.time() @@ -55,24 +77,26 @@ def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_pa # waiting for packets if total_size > 0: percent = float(bytes_so_far) / total_size - percent = round(percent*100, 2) + percent = round(percent * 100, 2) t_s = get_size(total_size) try: b_s = get_size(bytes_so_far, t_s.split(" ")[1]) except: b_s = get_size(bytes_so_far) perc_str = " {:.2f}%".format(percent) - bar_width = (TERMINAL_WIDTH // 3)-len(perc_str) - progress = "=" * int(bar_width * (percent/100)) - sys.stdout.write("\r\033[K{}/{} | {}{}{}{}{}".format( - b_s, - t_s, - progress, - " " * (bar_width-len(progress)), - perc_str, - speed, - remaining - )) + bar_width = (TERMINAL_WIDTH // 3) - len(perc_str) + progress = "=" * int(bar_width * (percent / 100)) + sys.stdout.write( + "\r\033[K{}/{} | {}{}{}{}{}".format( + b_s, + t_s, + progress, + " " * (bar_width - len(progress)), + perc_str, + speed, + remaining, + ) + ) else: b_s = get_size(bytes_so_far) sys.stdout.write("\r\033[K{}{}".format(b_s, speed)) @@ -85,7 +109,7 @@ def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_pa # If "DONE" is passed, we assume the download # finished - and bail if packet == "DONE": - print("") # Jump to the next line + print("") # Jump to the next line return # Append our packet to the list and ensure we're not # beyond our max. @@ -102,30 +126,32 @@ def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_pa speed = " | 0 B/s" remaining = " | ?? left" if total_size > 0 else "" except KeyboardInterrupt: - print("") # Jump to the next line + print("") # Jump to the next line return # If we have packets and it's time for an update, process # the info. update_check = time.time() if packets and update_check - last_update >= update_interval: - last_update = update_check # Refresh our update timestamp + last_update = update_check # Refresh our update timestamp speed = " | ?? B/s" if len(packets) > 1: # Let's calculate the amount downloaded over how long try: - first,last = packets[0][0],packets[-1][0] + first, last = packets[0][0], packets[-1][0] chunks = sum([float(x[1]) for x in packets]) - t = last-first + t = last - first assert t >= 0 - bytes_speed = 1. / t * chunks - speed = " | {}/s".format(get_size(bytes_speed,round_to=1)) + bytes_speed = 1.0 / t * chunks + speed = " | {}/s".format(get_size(bytes_speed, round_to=1)) # Get our remaining time if total_size > 0: - seconds_left = (total_size-bytes_so_far) / bytes_speed - days = seconds_left // 86400 - hours = (seconds_left - (days*86400)) // 3600 - mins = (seconds_left - (days*86400) - (hours*3600)) // 60 - secs = seconds_left - (days*86400) - (hours*3600) - (mins*60) + seconds_left = (total_size - bytes_so_far) / bytes_speed + days = seconds_left // 86400 + hours = (seconds_left - (days * 86400)) // 3600 + mins = (seconds_left - (days * 86400) - (hours * 3600)) // 60 + secs = ( + seconds_left - (days * 86400) - (hours * 3600) - (mins * 60) + ) if days > 99 or bytes_speed == 0: remaining = " | ?? left" else: @@ -133,70 +159,37 @@ def _process_hook(queue, total_size, bytes_so_far=0, update_interval=1.0, max_pa "{}:".format(int(days)) if days else "", int(hours), int(mins), - int(round(secs)) + int(round(secs)), ) - except: + except Exception: pass # Clear the packets so we don't reuse the same ones packets = [] + class Downloader: - def __init__(self,**kwargs): - self.ua = kwargs.get("useragent",{"User-Agent":"Mozilla"}) - self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB - if os.name=="nt": os.system("color") # Initialize cmd for ANSI escapes - # Provide reasonable default logic to workaround macOS CA file handling + def __init__(self, **kwargs): + self.ua = kwargs.get("useragent", {"User-Agent": "Mozilla"}) + self.chunk = 1048576 # 1024 x 1024 i.e. 1MiB + if os.name == "nt": + os.system("color") # Initialize cmd for ANSI escapes + # Provide reasonable default logic to workaround macOS CA file handling cafile = ssl.get_default_verify_paths().openssl_cafile try: # If default OpenSSL CA file does not exist, use that from certifi if not os.path.exists(cafile): import certifi + cafile = certifi.where() self.ssl_context = ssl.create_default_context(cafile=cafile) - except: + except Exception: # None of the above worked, disable certificate verification for now self.ssl_context = ssl._create_unverified_context() - return - - def _decode(self, value, encoding="utf-8", errors="ignore"): - # Helper method to only decode if bytes type - if sys.version_info >= (3,0) and isinstance(value, bytes): - return value.decode(encoding,errors) - return value - def _update_main_name(self): - # Windows running python 2 seems to have issues with multiprocessing - # if the case of the main script's name is incorrect: - # e.g. Downloader.py vs downloader.py - # - # To work around this, we try to scrape for the correct case if - # possible. - try: - path = os.path.abspath(sys.modules["__main__"].__file__) - except AttributeError as e: - # This likely means we're running from the interpreter - # directly - return None - if not os.path.isfile(path): - return None - # Get the file name and folder path - name = os.path.basename(path).lower() - fldr = os.path.dirname(path) - # Walk the files in the folder until we find our - # name - then steal its case and update that path - for f in os.listdir(fldr): - if f.lower() == name: - # Got it - new_path = os.path.join(fldr,f) - sys.modules["__main__"].__file__ = new_path - return new_path - # If we got here, it wasn't found - return None - - def _get_headers(self, headers = None): + def _get_headers(self, headers=None): # Fall back on the default ua if none provided - target = headers if isinstance(headers,dict) else self.ua + target = headers if isinstance(headers, dict) else self.ua new_headers = {} # Shallow copy to prevent changes to the headers # overriding the original @@ -204,59 +197,61 @@ def _get_headers(self, headers = None): new_headers[k] = target[k] return new_headers - def open_url(self, url, headers = None): + def open_url(self, url, headers=None): headers = self._get_headers(headers) # Wrap up the try/except block so we don't have to do this for each function try: response = urlopen(Request(url, headers=headers), context=self.ssl_context) - except Exception as e: + except Exception: # No fixing this - bail return None return response def get_size(self, *args, **kwargs): - return get_size(*args,**kwargs) + return get_size(*args, **kwargs) - def get_string(self, url, progress = True, headers = None, expand_gzip = True): - response = self.get_bytes(url,progress,headers,expand_gzip) - if response is None: return None - return self._decode(response) + def get_string(self, url, progress=True, headers=None, expand_gzip=True): + response = self.get_bytes(url, progress, headers, expand_gzip) + if response is None: + return None + return response.decode() - def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): + def get_bytes(self, url, progress=True, headers=None, expand_gzip=True): response = self.open_url(url, headers) - if response is None: return None - try: total_size = int(response.headers['Content-Length']) - except: total_size = -1 + if response is None: + return None + try: + total_size = int(response.headers["Content-Length"]) + except Exception: + total_size = -1 chunk_so_far = b"" - packets = queue = process = None + queue = process = None if progress: - # Make sure our vars are initialized - packets = [] if progress else None queue = multiprocessing.Queue() # Create the multiprocess and start it process = multiprocessing.Process( - target=_process_hook, - args=(queue,total_size) + target=_process_hook, args=(queue, total_size) ) process.daemon = True - # Filthy hack for earlier python versions on Windows - if os.name == "nt" and hasattr(multiprocessing,"forking"): - self._update_main_name() process.start() try: while True: chunk = response.read(self.chunk) if progress: # Add our items to the queue - queue.put((time.time(),len(chunk))) - if not chunk: break + queue.put((time.time(), len(chunk))) + if not chunk: + break chunk_so_far += chunk finally: # Close the response whenever we're done response.close() - if expand_gzip and response.headers.get("Content-Encoding","unknown").lower() == "gzip": + if ( + expand_gzip + and response.headers.get("Content-Encoding", "unknown").lower() == "gzip" + ): fileobj = BytesIO(chunk_so_far) - gfile = gzip.GzipFile(fileobj=fileobj) + gfile = gzip.GzipFile(fileobj=fileobj) return gfile.read() if progress: # Finalize the queue and wait @@ -264,13 +259,23 @@ def get_bytes(self, url, progress = True, headers = None, expand_gzip = True): process.join() return chunk_so_far - def stream_to_file(self, url, file_path, progress = True, headers = None, ensure_size_if_present = True, allow_resume = False): + def stream_to_file( + self, + url, + file_path, + progress=True, + headers=None, + ensure_size_if_present=True, + allow_resume=False, + ): response = self.open_url(url, headers) - if response is None: return None + if response is None: + return None bytes_so_far = 0 - try: total_size = int(response.headers['Content-Length']) - except: total_size = -1 - packets = queue = process = None + try: + total_size = int(response.headers["Content-Length"]) + except Exception: + total_size = -1 mode = "wb" if allow_resume and os.path.isfile(file_path) and total_size != -1: # File exists, we're resuming and have a target size. Check the @@ -283,7 +288,7 @@ def stream_to_file(self, url, file_path, progress = True, headers = None, ensure response.close() # File is not complete - seek to our current size bytes_so_far = current_size - mode = "ab" # Append + mode = "ab" # Append # We also need to try creating a new request # in order to pass our range header new_headers = self._get_headers(headers) @@ -291,30 +296,27 @@ def stream_to_file(self, url, file_path, progress = True, headers = None, ensure byte_string = "bytes={}-".format(current_size) new_headers["Range"] = byte_string response = self.open_url(url, new_headers) - if response is None: return None + if response is None: + return None + queue = process = None if progress: - # Make sure our vars are initialized - packets = [] if progress else None queue = multiprocessing.Queue() # Create the multiprocess and start it process = multiprocessing.Process( - target=_process_hook, - args=(queue,total_size,bytes_so_far) + target=_process_hook, args=(queue, total_size, bytes_so_far) ) process.daemon = True - # Filthy hack for earlier python versions on Windows - if os.name == "nt" and hasattr(multiprocessing,"forking"): - self._update_main_name() process.start() - with open(file_path,mode) as f: + with open(file_path, mode) as f: try: while True: chunk = response.read(self.chunk) bytes_so_far += len(chunk) if progress: # Add our items to the queue - queue.put((time.time(),len(chunk))) - if not chunk: break + queue.put((time.time(), len(chunk))) + if not chunk: + break f.write(chunk) finally: # Close the response whenever we're done @@ -326,5 +328,5 @@ def stream_to_file(self, url, file_path, progress = True, headers = None, ensure if ensure_size_if_present and total_size != -1: # We're verifying size - make sure we got what we asked for if bytes_so_far != total_size: - return None # We didn't - imply it failed + return None # We didn't - imply it failed return file_path if os.path.exists(file_path) else None diff --git a/Scripts/plist.py b/Scripts/plist.py index c6274dd..ef59dbc 100755 --- a/Scripts/plist.py +++ b/Scripts/plist.py @@ -1,440 +1,220 @@ -### ### -# Imports # -### ### - -import datetime, os, plistlib, struct, sys, itertools, binascii +import datetime +import os +import plistlib +import struct +import sys +import itertools +import binascii from io import BytesIO +from collections import OrderedDict -if sys.version_info < (3,0): - # Force use of StringIO instead of cStringIO as the latter - # has issues with Unicode strings - from StringIO import StringIO -else: - from io import StringIO - -try: - basestring # Python 2 - unicode -except NameError: - basestring = str # Python 3 - unicode = str - -try: - FMT_XML = plistlib.FMT_XML - FMT_BINARY = plistlib.FMT_BINARY -except AttributeError: - FMT_XML = "FMT_XML" - FMT_BINARY = "FMT_BINARY" - -### ### -# Helper Methods # -### ### - -def wrap_data(value): - if not _check_py3(): return plistlib.Data(value) - return value +FMT_XML = plistlib.FMT_XML +FMT_BINARY = plistlib.FMT_BINARY -def extract_data(value): - if not _check_py3() and isinstance(value,plistlib.Data): return value.data - return value -def _check_py3(): - return sys.version_info >= (3, 0) +class InvalidFileException(ValueError): + def __init__(self, message="Invalid file"): + super().__init__(message) + + +_BINARY_FORMAT = {1: "B", 2: "H", 4: "L", 8: "Q"} + +_undefined = object() + + +class UID(plistlib.UID): + pass + def _is_binary(fp): - if isinstance(fp, basestring): + if isinstance(fp, (bytes, bytearray)): return fp.startswith(b"bplist00") header = fp.read(32) fp.seek(0) - return header[:8] == b'bplist00' + return header[:8] == b"bplist00" + def _seek_past_whitespace(fp): - offset = 0 + offset = fp.tell() while True: byte = fp.read(1) if not byte: - # End of file, reset offset and bail - offset = 0 + offset = fp.tell() break if not byte.isspace(): - # Found our first non-whitespace character break - offset += 1 - # Seek to the first non-whitespace char + offset = fp.tell() fp.seek(offset) return offset -### ### -# Deprecated Functions - Remapped # -### ### - -def readPlist(pathOrFile): - if not isinstance(pathOrFile, basestring): - return load(pathOrFile) - with open(pathOrFile, "rb") as f: - return load(f) - -def writePlist(value, pathOrFile): - if not isinstance(pathOrFile, basestring): - return dump(value, pathOrFile, fmt=FMT_XML, sort_keys=True, skipkeys=False) - with open(pathOrFile, "wb") as f: - return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) - -### ### -# Remapped Functions # -### ### -def load(fp, fmt=None, use_builtin_types=None, dict_type=dict): +def load(fp, fmt=None, dict_type=OrderedDict): if _is_binary(fp): - use_builtin_types = False if use_builtin_types is None else use_builtin_types - try: - p = _BinaryPlistParser(use_builtin_types=use_builtin_types, dict_type=dict_type) - except: - # Python 3.9 removed use_builtin_types - p = _BinaryPlistParser(dict_type=dict_type) + p = _BinaryPlistParser(dict_type=dict_type) return p.parse(fp) - elif _check_py3(): - offset = _seek_past_whitespace(fp) - use_builtin_types = True if use_builtin_types is None else use_builtin_types - # We need to monkey patch this to allow for hex integers - code taken/modified from - # https://github.com/python/cpython/blob/3.8/Lib/plistlib.py - if fmt is None: - header = fp.read(32) - fp.seek(offset) - for info in plistlib._FORMATS.values(): - if info['detect'](header): - P = info['parser'] - break - else: - raise plistlib.InvalidFileException() + offset = _seek_past_whitespace(fp) + if fmt is None: + header = fp.read(32) + fp.seek(offset) + for info in plistlib._FORMATS.values(): + if info["detect"](header): + P = info["parser"] + break else: - P = plistlib._FORMATS[fmt]['parser'] - try: - p = P(use_builtin_types=use_builtin_types, dict_type=dict_type) - except: - # Python 3.9 removed use_builtin_types - p = P(dict_type=dict_type) - if isinstance(p,plistlib._PlistParser): - # Monkey patch! - def end_integer(): - d = p.get_data() - value = int(d,16) if d.lower().startswith("0x") else int(d) - if -1 << 63 <= value < 1 << 64: - p.add_object(value) - else: - raise OverflowError("Integer overflow at line {}".format(p.parser.CurrentLineNumber)) - def end_data(): - try: - p.add_object(plistlib._decode_base64(p.get_data())) - except Exception as e: - raise Exception("Data error at line {}: {}".format(p.parser.CurrentLineNumber,e)) - p.end_integer = end_integer - p.end_data = end_data - return p.parse(fp) + raise plistlib.InvalidFileException() else: - offset = _seek_past_whitespace(fp) - # Is not binary - assume a string - and try to load - # We avoid using readPlistFromString() as that uses - # cStringIO and fails when Unicode strings are detected - # Don't subclass - keep the parser local - from xml.parsers.expat import ParserCreate - # Create a new PlistParser object - then we need to set up - # the values and parse. - p = plistlib.PlistParser() - parser = ParserCreate() - parser.StartElementHandler = p.handleBeginElement - parser.EndElementHandler = p.handleEndElement - parser.CharacterDataHandler = p.handleData - # We also need to monkey patch this to allow for other dict_types, hex int support - # proper line output for data errors, and for unicode string decoding - def begin_dict(attrs): - d = dict_type() - p.addObject(d) - p.stack.append(d) + P = plistlib._FORMATS[fmt]["parser"] + p = P(dict_type=dict_type) + if isinstance(p, plistlib._PlistParser): + def end_integer(): - d = p.getData() - value = int(d,16) if d.lower().startswith("0x") else int(d) + d = p.get_data() + value = int(d, 16) if d.lower().startswith("0x") else int(d) if -1 << 63 <= value < 1 << 64: - p.addObject(value) + p.add_object(value) else: - raise OverflowError("Integer overflow at line {}".format(parser.CurrentLineNumber)) + raise OverflowError( + f"Integer overflow at line {p.parser.CurrentLineNumber}" + ) + def end_data(): try: - p.addObject(plistlib.Data.fromBase64(p.getData())) + p.add_object(plistlib._decode_base64(p.get_data())) except Exception as e: - raise Exception("Data error at line {}: {}".format(parser.CurrentLineNumber,e)) - def end_string(): - d = p.getData() - if isinstance(d,unicode): - d = d.encode("utf-8") - p.addObject(d) - p.begin_dict = begin_dict + raise Exception(f"Data error at line {p.parser.CurrentLineNumber}: {e}") + p.end_integer = end_integer p.end_data = end_data - p.end_string = end_string - if isinstance(fp, unicode): - # Encode unicode -> string; use utf-8 for safety - fp = fp.encode("utf-8") - if isinstance(fp, basestring): - # It's a string - let's wrap it up - fp = StringIO(fp) - # Parse it - parser.ParseFile(fp) - return p.root - -def loads(value, fmt=None, use_builtin_types=None, dict_type=dict): - if _check_py3() and isinstance(value, basestring): - # If it's a string - encode it - value = value.encode() - try: - return load(BytesIO(value),fmt=fmt,use_builtin_types=use_builtin_types,dict_type=dict_type) - except: - # Python 3.9 removed use_builtin_types - return load(BytesIO(value),fmt=fmt,dict_type=dict_type) + return p.parse(fp) + + +def loads(value, fmt=None, dict_type=OrderedDict): + if isinstance(value, str): + value = value.encode("utf-8") + return load(BytesIO(value), fmt=fmt, dict_type=dict_type) + def dump(value, fp, fmt=FMT_XML, sort_keys=True, skipkeys=False): if fmt == FMT_BINARY: - # Assume binary at this point writer = _BinaryPlistWriter(fp, sort_keys=sort_keys, skipkeys=skipkeys) writer.write(value) elif fmt == FMT_XML: - if _check_py3(): - plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) - else: - # We need to monkey patch a bunch here too in order to avoid auto-sorting - # of keys - writer = plistlib.PlistWriter(fp) - def writeDict(d): - if d: - writer.beginElement("dict") - items = sorted(d.items()) if sort_keys else d.items() - for key, value in items: - if not isinstance(key, basestring): - if skipkeys: - continue - raise TypeError("keys must be strings") - writer.simpleElement("key", key) - writer.writeValue(value) - writer.endElement("dict") - else: - writer.simpleElement("dict") - writer.writeDict = writeDict - writer.writeln("") - writer.writeValue(value) - writer.writeln("") + plistlib.dump(value, fp, fmt=fmt, sort_keys=sort_keys, skipkeys=skipkeys) else: - # Not a proper format - raise ValueError("Unsupported format: {}".format(fmt)) - + raise ValueError(f"Unsupported format: {fmt}") + + def dumps(value, fmt=FMT_XML, skipkeys=False, sort_keys=True): - # We avoid using writePlistToString() as that uses - # cStringIO and fails when Unicode strings are detected - f = BytesIO() if _check_py3() else StringIO() + f = BytesIO() dump(value, f, fmt=fmt, skipkeys=skipkeys, sort_keys=sort_keys) value = f.getvalue() - if _check_py3(): + if fmt == FMT_XML: value = value.decode("utf-8") return value -### ### -# Binary Plist Stuff For Py2 # -### ### - -# From the python 3 plistlib.py source: https://github.com/python/cpython/blob/3.11/Lib/plistlib.py -# Tweaked to function on both Python 2 and 3 - -class UID: - def __init__(self, data): - if not isinstance(data, int): - raise TypeError("data must be an int") - # It seems Apple only uses 32-bit unsigned ints for UIDs. Although the comment in - # CoreFoundation's CFBinaryPList.c detailing the binary plist format theoretically - # allows for 64-bit UIDs, most functions in the same file use 32-bit unsigned ints, - # with the sole function hinting at 64-bits appearing to be a leftover from copying - # and pasting integer handling code internally, and this code has not changed since - # it was added. (In addition, code in CFPropertyList.c to handle CF$UID also uses a - # 32-bit unsigned int.) - # - # if data >= 1 << 64: - # raise ValueError("UIDs cannot be >= 2**64") - if data >= 1 << 32: - raise ValueError("UIDs cannot be >= 2**32 (4294967296)") - if data < 0: - raise ValueError("UIDs must be positive") - self.data = data - - def __index__(self): - return self.data - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self.data)) - - def __reduce__(self): - return self.__class__, (self.data,) - - def __eq__(self, other): - if not isinstance(other, UID): - return NotImplemented - return self.data == other.data - - def __hash__(self): - return hash(self.data) - -class InvalidFileException (ValueError): - def __init__(self, message="Invalid file"): - ValueError.__init__(self, message) -_BINARY_FORMAT = {1: 'B', 2: 'H', 4: 'L', 8: 'Q'} +def readPlist(pathOrFile): + with open(pathOrFile, "rb") as f: + return load(f) + + +def writePlist(value, pathOrFile): + with open(pathOrFile, "wb") as f: + return dump(value, f, fmt=FMT_XML, sort_keys=True, skipkeys=False) -_undefined = object() class _BinaryPlistParser: - """ - Read or write a binary plist file, following the description of the binary - format. Raise InvalidFileException in case of error, otherwise return the - root object. - see also: http://opensource.apple.com/source/CF/CF-744.18/CFBinaryPList.c - """ - def __init__(self, use_builtin_types, dict_type): - self._use_builtin_types = use_builtin_types + def __init__(self, dict_type): self._dict_type = dict_type def parse(self, fp): try: - # The basic file format: - # HEADER - # object... - # refid->offset... - # TRAILER self._fp = fp self._fp.seek(-32, os.SEEK_END) trailer = self._fp.read(32) if len(trailer) != 32: raise InvalidFileException() ( - offset_size, self._ref_size, num_objects, top_object, - offset_table_offset - ) = struct.unpack('>6xBBQQQ', trailer) + offset_size, + self._ref_size, + num_objects, + top_object, + offset_table_offset, + ) = struct.unpack(">6xBBQQQ", trailer) self._fp.seek(offset_table_offset) self._object_offsets = self._read_ints(num_objects, offset_size) self._objects = [_undefined] * num_objects return self._read_object(top_object) - - except (OSError, IndexError, struct.error, OverflowError, - UnicodeDecodeError): + except (OSError, IndexError, struct.error, OverflowError, UnicodeDecodeError): raise InvalidFileException() def _get_size(self, tokenL): - """ return the size of the next object.""" if tokenL == 0xF: m = self._fp.read(1)[0] - if not _check_py3(): - m = ord(m) m = m & 0x3 s = 1 << m - f = '>' + _BINARY_FORMAT[s] + f = ">" + _BINARY_FORMAT[s] return struct.unpack(f, self._fp.read(s))[0] - return tokenL def _read_ints(self, n, size): data = self._fp.read(size * n) if size in _BINARY_FORMAT: - return struct.unpack('>' + _BINARY_FORMAT[size] * n, data) + return struct.unpack(">" + _BINARY_FORMAT[size] * n, data) else: if not size or len(data) != size * n: raise InvalidFileException() - return tuple(int(binascii.hexlify(data[i: i + size]),16) - for i in range(0, size * n, size)) - '''return tuple(int.from_bytes(data[i: i + size], 'big') - for i in range(0, size * n, size))''' + return tuple( + int.from_bytes(data[i : i + size], "big") + for i in range(0, size * n, size) + ) def _read_refs(self, n): return self._read_ints(n, self._ref_size) def _read_object(self, ref): - """ - read the object by reference. - May recursively read sub-objects (content of an array/dict/set) - """ result = self._objects[ref] if result is not _undefined: return result - offset = self._object_offsets[ref] self._fp.seek(offset) token = self._fp.read(1)[0] - if not _check_py3(): - token = ord(token) tokenH, tokenL = token & 0xF0, token & 0x0F - - if token == 0x00: # \x00 or 0x00 + if token == 0x00: result = None - - elif token == 0x08: # \x08 or 0x08 + elif token == 0x08: result = False - - elif token == 0x09: # \x09 or 0x09 + elif token == 0x09: result = True - - # The referenced source code also mentions URL (0x0c, 0x0d) and - # UUID (0x0e), but neither can be generated using the Cocoa libraries. - - elif token == 0x0f: # \x0f or 0x0f - result = b'' - - elif tokenH == 0x10: # int - result = int(binascii.hexlify(self._fp.read(1 << tokenL)),16) - if tokenL >= 3: # Signed - adjust - result = result-((result & 0x8000000000000000) << 1) - - elif token == 0x22: # real - result = struct.unpack('>f', self._fp.read(4))[0] - - elif token == 0x23: # real - result = struct.unpack('>d', self._fp.read(8))[0] - - elif token == 0x33: # date - f = struct.unpack('>d', self._fp.read(8))[0] - # timestamp 0 of binary plists corresponds to 1/1/2001 - # (year of Mac OS X 10.0), instead of 1/1/1970. - result = (datetime.datetime(2001, 1, 1) + - datetime.timedelta(seconds=f)) - - elif tokenH == 0x40: # data + elif token == 0x0F: + result = b"" + elif tokenH == 0x10: + result = int.from_bytes(self._fp.read(1 << tokenL), "big", signed=True) + elif token == 0x22: + result = struct.unpack(">f", self._fp.read(4))[0] + elif token == 0x23: + result = struct.unpack(">d", self._fp.read(8))[0] + elif token == 0x33: + f = struct.unpack(">d", self._fp.read(8))[0] + result = datetime.datetime(2001, 1, 1) + datetime.timedelta(seconds=f) + elif tokenH == 0x40: s = self._get_size(tokenL) - if self._use_builtin_types or not hasattr(plistlib, "Data"): - result = self._fp.read(s) - else: - result = plistlib.Data(self._fp.read(s)) - - elif tokenH == 0x50: # ascii string + result = self._fp.read(s) + elif tokenH == 0x50: s = self._get_size(tokenL) - result = self._fp.read(s).decode('ascii') - result = result - - elif tokenH == 0x60: # unicode string + result = self._fp.read(s).decode("ascii") + elif tokenH == 0x60: s = self._get_size(tokenL) - result = self._fp.read(s * 2).decode('utf-16be') - - elif tokenH == 0x80: # UID - # used by Key-Archiver plist files - result = UID(int(binascii.hexlify(self._fp.read(1 + tokenL)),16)) - - elif tokenH == 0xA0: # array + result = self._fp.read(s * 2).decode("utf-16be") + elif tokenH == 0x80: + result = UID(int.from_bytes(self._fp.read(1 + tokenL), "big")) + elif tokenH == 0xA0: s = self._get_size(tokenL) obj_refs = self._read_refs(s) result = [] self._objects[ref] = result result.extend(self._read_object(x) for x in obj_refs) - - # tokenH == 0xB0 is documented as 'ordset', but is not actually - # implemented in the Apple reference code. - - # tokenH == 0xC0 is documented as 'set', but sets cannot be used in - # plists. - - elif tokenH == 0xD0: # dict + elif tokenH == 0xD0: s = self._get_size(tokenL) key_refs = self._read_refs(s) obj_refs = self._read_refs(s) @@ -442,126 +222,86 @@ def _read_object(self, ref): self._objects[ref] = result for k, o in zip(key_refs, obj_refs): key = self._read_object(k) - if hasattr(plistlib, "Data") and isinstance(key, plistlib.Data): - key = key.data result[key] = self._read_object(o) - else: raise InvalidFileException() - self._objects[ref] = result return result + def _count_to_size(count): if count < 1 << 8: return 1 - elif count < 1 << 16: return 2 - elif count < 1 << 32: return 4 - else: return 8 + _scalars = (str, int, float, datetime.datetime, bytes) -class _BinaryPlistWriter (object): + +class _BinaryPlistWriter(object): def __init__(self, fp, sort_keys, skipkeys): self._fp = fp self._sort_keys = sort_keys self._skipkeys = skipkeys def write(self, value): - - # Flattened object list: self._objlist = [] - - # Mappings from object->objectid - # First dict has (type(object), object) as the key, - # second dict is used when object is not hashable and - # has id(object) as the key. self._objtable = {} self._objidtable = {} - - # Create list of all objects in the plist self._flatten(value) - - # Size of object references in serialized containers - # depends on the number of objects in the plist. num_objects = len(self._objlist) - self._object_offsets = [0]*num_objects + self._object_offsets = [0] * num_objects self._ref_size = _count_to_size(num_objects) - self._ref_format = _BINARY_FORMAT[self._ref_size] - - # Write file header - self._fp.write(b'bplist00') - - # Write object list + self._fp.write(b"bplist00") for obj in self._objlist: self._write_object(obj) - - # Write refnum->object offset table top_object = self._getrefnum(value) offset_table_offset = self._fp.tell() offset_size = _count_to_size(offset_table_offset) - offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects + offset_format = ">" + _BINARY_FORMAT[offset_size] * num_objects self._fp.write(struct.pack(offset_format, *self._object_offsets)) - - # Write trailer sort_version = 0 trailer = ( - sort_version, offset_size, self._ref_size, num_objects, - top_object, offset_table_offset + sort_version, + offset_size, + self._ref_size, + num_objects, + top_object, + offset_table_offset, ) - self._fp.write(struct.pack('>5xBBBQQQ', *trailer)) + self._fp.write(struct.pack(">5xBBBQQQ", *trailer)) def _flatten(self, value): - # First check if the object is in the object table, not used for - # containers to ensure that two subcontainers with the same contents - # will be serialized as distinct values. if isinstance(value, _scalars): if (type(value), value) in self._objtable: return - - elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): - if (type(value.data), value.data) in self._objtable: - return - elif id(value) in self._objidtable: return - - # Add to objectreference map refnum = len(self._objlist) self._objlist.append(value) if isinstance(value, _scalars): self._objtable[(type(value), value)] = refnum - elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): - self._objtable[(type(value.data), value.data)] = refnum else: self._objidtable[id(value)] = refnum - - # And finally recurse into containers if isinstance(value, dict): keys = [] values = [] - items = value.items() - if self._sort_keys: - items = sorted(items) - + items = sorted(value.items()) if self._sort_keys else value.items() for k, v in items: - if not isinstance(k, basestring): + if not isinstance(k, str): if self._skipkeys: continue raise TypeError("keys must be strings") keys.append(k) values.append(v) - for o in itertools.chain(keys, values): self._flatten(o) - elif isinstance(value, (list, tuple)): for o in value: self._flatten(o) @@ -569,120 +309,93 @@ def _flatten(self, value): def _getrefnum(self, value): if isinstance(value, _scalars): return self._objtable[(type(value), value)] - elif hasattr(plistlib, "Data") and isinstance(value, plistlib.Data): - return self._objtable[(type(value.data), value.data)] else: return self._objidtable[id(value)] def _write_size(self, token, size): if size < 15: - self._fp.write(struct.pack('>B', token | size)) - + self._fp.write(struct.pack(">B", token | size)) elif size < 1 << 8: - self._fp.write(struct.pack('>BBB', token | 0xF, 0x10, size)) - + self._fp.write(struct.pack(">BBB", token | 0xF, 0x10, size)) elif size < 1 << 16: - self._fp.write(struct.pack('>BBH', token | 0xF, 0x11, size)) - + self._fp.write(struct.pack(">BBH", token | 0xF, 0x11, size)) elif size < 1 << 32: - self._fp.write(struct.pack('>BBL', token | 0xF, 0x12, size)) - + self._fp.write(struct.pack(">BBL", token | 0xF, 0x12, size)) else: - self._fp.write(struct.pack('>BBQ', token | 0xF, 0x13, size)) + self._fp.write(struct.pack(">BBQ", token | 0xF, 0x13, size)) def _write_object(self, value): ref = self._getrefnum(value) self._object_offsets[ref] = self._fp.tell() if value is None: - self._fp.write(b'\x00') - + self._fp.write(b"\x00") elif value is False: - self._fp.write(b'\x08') - + self._fp.write(b"\x08") elif value is True: - self._fp.write(b'\x09') - + self._fp.write(b"\x09") elif isinstance(value, int): if value < 0: try: - self._fp.write(struct.pack('>Bq', 0x13, value)) + self._fp.write(struct.pack(">Bq", 0x13, value)) except struct.error: - raise OverflowError(value) # from None + raise OverflowError(value) elif value < 1 << 8: - self._fp.write(struct.pack('>BB', 0x10, value)) + self._fp.write(struct.pack(">BB", 0x10, value)) elif value < 1 << 16: - self._fp.write(struct.pack('>BH', 0x11, value)) + self._fp.write(struct.pack(">BH", 0x11, value)) elif value < 1 << 32: - self._fp.write(struct.pack('>BL', 0x12, value)) + self._fp.write(struct.pack(">BL", 0x12, value)) elif value < 1 << 63: - self._fp.write(struct.pack('>BQ', 0x13, value)) + self._fp.write(struct.pack(">BQ", 0x13, value)) elif value < 1 << 64: - self._fp.write(b'\x14' + value.to_bytes(16, 'big', signed=True)) + self._fp.write(b"\x14" + value.to_bytes(16, "big", signed=True)) else: raise OverflowError(value) - elif isinstance(value, float): - self._fp.write(struct.pack('>Bd', 0x23, value)) - + self._fp.write(struct.pack(">Bd", 0x23, value)) elif isinstance(value, datetime.datetime): f = (value - datetime.datetime(2001, 1, 1)).total_seconds() - self._fp.write(struct.pack('>Bd', 0x33, f)) - - elif (_check_py3() and isinstance(value, (bytes, bytearray))) or (hasattr(plistlib, "Data") and isinstance(value, plistlib.Data)): - if not isinstance(value, (bytes, bytearray)): - value = value.data # Unpack it + self._fp.write(struct.pack(">Bd", 0x33, f)) + elif isinstance(value, bytes): self._write_size(0x40, len(value)) self._fp.write(value) - - elif isinstance(value, basestring): + elif isinstance(value, str): try: - t = value.encode('ascii') + t = value.encode("ascii") self._write_size(0x50, len(value)) except UnicodeEncodeError: - t = value.encode('utf-16be') + t = value.encode("utf-16be") self._write_size(0x60, len(t) // 2) self._fp.write(t) - - elif isinstance(value, UID) or (hasattr(plistlib,"UID") and isinstance(value, plistlib.UID)): + elif isinstance(value, plistlib.UID): if value.data < 0: raise ValueError("UIDs must be positive") elif value.data < 1 << 8: - self._fp.write(struct.pack('>BB', 0x80, value)) + self._fp.write(struct.pack(">BB", 0x80, value.data)) elif value.data < 1 << 16: - self._fp.write(struct.pack('>BH', 0x81, value)) + self._fp.write(struct.pack(">BH", 0x81, value.data)) elif value.data < 1 << 32: - self._fp.write(struct.pack('>BL', 0x83, value)) - # elif value.data < 1 << 64: - # self._fp.write(struct.pack('>BQ', 0x87, value)) + self._fp.write(struct.pack(">BL", 0x83, value.data)) else: raise OverflowError(value) - elif isinstance(value, (list, tuple)): refs = [self._getrefnum(o) for o in value] s = len(refs) self._write_size(0xA0, s) - self._fp.write(struct.pack('>' + self._ref_format * s, *refs)) - + self._fp.write(struct.pack(">" + self._ref_format * s, *refs)) elif isinstance(value, dict): keyRefs, valRefs = [], [] - - if self._sort_keys: - rootItems = sorted(value.items()) - else: - rootItems = value.items() - - for k, v in rootItems: - if not isinstance(k, basestring): + items = sorted(value.items()) if self._sort_keys else value.items() + for k, v in items: + if not isinstance(k, str): if self._skipkeys: continue raise TypeError("keys must be strings") keyRefs.append(self._getrefnum(k)) valRefs.append(self._getrefnum(v)) - s = len(keyRefs) self._write_size(0xD0, s) - self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs)) - self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs)) - + self._fp.write(struct.pack(">" + self._ref_format * s, *keyRefs)) + self._fp.write(struct.pack(">" + self._ref_format * s, *valRefs)) else: raise TypeError(value) diff --git a/Scripts/run.py b/Scripts/run.py index b586adc..9c3365d 100755 --- a/Scripts/run.py +++ b/Scripts/run.py @@ -1,19 +1,21 @@ -import sys, subprocess, time, threading, shlex -try: - from Queue import Queue, Empty -except: - from queue import Queue, Empty +import sys +import subprocess +import time +import threading +import shlex +from queue import Queue, Empty + +ON_POSIX = "posix" in sys.builtin_module_names -ON_POSIX = 'posix' in sys.builtin_module_names class Run: def __init__(self): - return + pass def _read_output(self, pipe, q): try: - for line in iter(lambda: pipe.read(1), b''): + for line in iter(lambda: pipe.read(1), ""): q.put(line) except ValueError: pass @@ -24,96 +26,109 @@ def _create_thread(self, output): q = Queue() t = threading.Thread(target=self._read_output, args=(output, q)) t.daemon = True - return (q,t) + return (q, t) - def _stream_output(self, comm, shell = False): + def _stream_output(self, comm, shell=False): output = error = "" p = None try: - if shell and type(comm) is list: + if shell and isinstance(comm, list): comm = " ".join(shlex.quote(x) for x in comm) - if not shell and type(comm) is str: + if not shell and isinstance(comm, str): comm = shlex.split(comm) - p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX) + p = subprocess.Popen( + comm, + shell=shell, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + text=True, + close_fds=ON_POSIX, + ) # Setup the stdout thread/queue - q,t = self._create_thread(p.stdout) - qe,te = self._create_thread(p.stderr) + q, t = self._create_thread(p.stdout) + qe, te = self._create_thread(p.stderr) # Start both threads t.start() te.start() while True: c = z = "" - try: c = q.get_nowait() - except Empty: pass + try: + c = q.get_nowait() + except Empty: + pass else: sys.stdout.write(c) output += c sys.stdout.flush() - try: z = qe.get_nowait() - except Empty: pass + try: + z = qe.get_nowait() + except Empty: + pass else: sys.stderr.write(z) error += z sys.stderr.flush() - if not c==z=="": continue # Keep going until empty - # No output - see if still running - p.poll() - if p.returncode != None: - # Subprocess ended - break - # No output, but subprocess still running - stall for 20ms - time.sleep(0.02) + if c == z == "": + # No output - see if still running + p.poll() + if p.returncode is not None: + # Subprocess ended + break + # No output, but subprocess still running - stall for 20ms + time.sleep(0.02) o, e = p.communicate() - return (output+o, error+e, p.returncode) - except: + return (output + o, error + e, p.returncode) + except Exception: if p: - try: o, e = p.communicate() - except: o = e = "" - return (output+o, error+e, p.returncode) + try: + o, e = p.communicate() + except Exception: + o = e = "" + return (output + o, error + e, p.returncode) return ("", "Command not found!", 1) - def _decode(self, value, encoding="utf-8", errors="ignore"): - # Helper method to only decode if bytes type - if sys.version_info >= (3,0) and isinstance(value, bytes): - return value.decode(encoding,errors) - return value - - def _run_command(self, comm, shell = False): + def _run_command(self, comm, shell=False): c = None try: - if shell and type(comm) is list: + if shell and isinstance(comm, list): comm = " ".join(shlex.quote(x) for x in comm) - if not shell and type(comm) is str: + if not shell and isinstance(comm, str): comm = shlex.split(comm) - p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p = subprocess.Popen( + comm, + shell=shell, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) c = p.communicate() - except: - if c == None: - return ("", "Command not found!", 1) - return (self._decode(c[0]), self._decode(c[1]), p.returncode) + return (c[0], c[1], p.returncode) + except Exception: + return ("", "Command not found!", 1) - def run(self, command_list, leave_on_fail = False): - # Command list should be an array of dicts - if type(command_list) is dict: + def run(self, command_list, leave_on_fail=False): + # Command list should be a list of dicts + if isinstance(command_list, dict): # We only have one command command_list = [command_list] output_list = [] for comm in command_list: - args = comm.get("args", []) - shell = comm.get("shell", False) + args = comm.get("args", []) + shell = comm.get("shell", False) stream = comm.get("stream", False) - sudo = comm.get("sudo", False) + sudo = comm.get("sudo", False) stdout = comm.get("stdout", False) stderr = comm.get("stderr", False) - mess = comm.get("message", None) - show = comm.get("show", False) - - if not mess == None: + mess = comm.get("message", None) + show = comm.get("show", False) + + if mess is not None: print(mess) - if not len(args): + if not args: # nothing to process continue if sudo: @@ -121,13 +136,13 @@ def run(self, command_list, leave_on_fail = False): out = self._run_command(["which", "sudo"]) if "sudo" in out[0]: # Can sudo - if type(args) is list: - args.insert(0, out[0].replace("\n", "")) # add to start of list - elif type(args) is str: - args = out[0].replace("\n", "") + " " + args # add to start of string - + if isinstance(args, list): + args.insert(0, out[0].strip()) # add to start of list + elif isinstance(args, str): + args = out[0].strip() + " " + args # add to start of string + if show: - print(" ".join(args)) + print(" ".join(args) if isinstance(args, list) else args) if stream: # Stream it! @@ -135,9 +150,9 @@ def run(self, command_list, leave_on_fail = False): else: # Just run and gather output out = self._run_command(args, shell) - if stdout and len(out[0]): + if stdout and out[0]: print(out[0]) - if stderr and len(out[1]): + if stderr and out[1]: print(out[1]) # Append output output_list.append(out) diff --git a/Scripts/utils.py b/Scripts/utils.py index 08a453f..9d8b48c 100755 --- a/Scripts/utils.py +++ b/Scripts/utils.py @@ -1,15 +1,21 @@ -import sys, os, time, re, json, datetime, ctypes, subprocess +import sys +import os +import time +import re +import json +import datetime +import ctypes +import subprocess if os.name == "nt": - # Windows import msvcrt else: - # Not Windows \o/ import select + class Utils: - def __init__(self, name = "Python Script"): + def __init__(self, name="Python Script"): self.name = name # Init our colors before we need to print anything cwd = os.getcwd() @@ -33,46 +39,48 @@ def elevate(self, file): if self.check_admin(): return if os.name == "nt": - ctypes.windll.shell32.ShellExecuteW(None, "runas", '"{}"'.format(sys.executable), '"{}"'.format(file), None, 1) + ctypes.windll.shell32.ShellExecuteW( + None, "runas", f'"{sys.executable}"', f'"{file}"', None, 1 + ) else: try: - p = subprocess.Popen(["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - c = p.communicate()[0].decode("utf-8", "ignore").replace("\n", "") - os.execv(c, [ sys.executable, 'python'] + sys.argv) - except: + p = subprocess.Popen( + ["which", "sudo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + c = p.communicate()[0].decode("utf-8", "ignore").strip() + os.execv(c, [sys.executable, "python"] + sys.argv) + except Exception: exit(1) - + def compare_versions(self, vers1, vers2, **kwargs): # Helper method to compare ##.## strings # # vers1 < vers2 = True # vers1 = vers2 = None # vers1 > vers2 = False - + # Sanitize the pads pad = str(kwargs.get("pad", "")) sep = str(kwargs.get("separator", ".")) ignore_case = kwargs.get("ignore_case", True) - + # Cast as strings vers1 = str(vers1) vers2 = str(vers2) - + if ignore_case: vers1 = vers1.lower() vers2 = vers2.lower() # Split and pad lists v1_parts, v2_parts = self.pad_length(vers1.split(sep), vers2.split(sep)) - + # Iterate and compare for i in range(len(v1_parts)): # Remove non-numeric - v1 = ''.join(c.lower() for c in v1_parts[i] if c.isalnum()) - v2 = ''.join(c.lower() for c in v2_parts[i] if c.isalnum()) - # Equalize the lengths - v1, v2 = self.pad_length(v1, v2) + v1 = "".join(c.lower() for c in v1_parts[i] if c.isalnum()) + v2 = "".join(c.lower() for c in v2_parts[i] if c.isalnum()) # Compare if str(v1) < str(v2): return True @@ -81,39 +89,38 @@ def compare_versions(self, vers1, vers2, **kwargs): # Never differed - return None, must be equal return None - def pad_length(self, var1, var2, pad = "0"): + def pad_length(self, var1, var2, pad="0"): # Pads the vars on the left side to make them equal length pad = "0" if len(str(pad)) < 1 else str(pad)[0] if not type(var1) == type(var2): # Type mismatch! Just return what we got return (var1, var2) if len(var1) < len(var2): - if type(var1) is list: - var1.extend([str(pad) for x in range(len(var2) - len(var1))]) + if isinstance(var1, list): + var1.extend([str(pad) for _ in range(len(var2) - len(var1))]) else: - var1 = "{}{}".format((pad*(len(var2)-len(var1))), var1) + var1 = "{}{}".format((pad * (len(var2) - len(var1))), var1) elif len(var2) < len(var1): - if type(var2) is list: - var2.extend([str(pad) for x in range(len(var1) - len(var2))]) + if isinstance(var2, list): + var2.extend([str(pad) for _ in range(len(var1) - len(var2))]) else: - var2 = "{}{}".format((pad*(len(var1)-len(var2))), var2) + var2 = "{}{}".format((pad * (len(var1) - len(var2))), var2) return (var1, var2) - + def check_path(self, path): # Let's loop until we either get a working path, or no changes test_path = path last_path = None while True: # Bail if we've looped at least once and the path didn't change - if last_path != None and last_path == test_path: return None + if last_path is not None and last_path == test_path: + return None last_path = test_path - # Check if we stripped everything out - if not len(test_path): return None # Check if we have a valid path if os.path.exists(test_path): return os.path.abspath(test_path) # Check for quotes - if test_path[0] == test_path[-1] and test_path[0] in ('"',"'"): + if test_path[0] == test_path[-1] and test_path[0] in ('"', "'"): test_path = test_path[1:-1] continue # Check for a tilde and expand if needed @@ -124,14 +131,16 @@ def check_path(self, path): test_path = tilde_expanded continue # Let's check for spaces - strip from the left first, then the right - if test_path[0] in (" ","\t"): + if test_path[0] in (" ", "\t"): test_path = test_path[1:] continue - if test_path[-1] in (" ","\t"): + if test_path[-1] in (" ", "\t"): test_path = test_path[:-1] continue # Maybe we have escapes to handle? - test_path = "\\".join([x.replace("\\", "") for x in test_path.split("\\\\")]) + test_path = "\\".join( + [x.replace("\\", "") for x in test_path.split("\\\\")] + ) def grab(self, prompt, **kwargs): # Takes a prompt, a default, and a timeout and shows it with that timeout @@ -140,43 +149,42 @@ def grab(self, prompt, **kwargs): default = kwargs.get("default", None) # If we don't have a timeout - then skip the timed sections if timeout <= 0: - if sys.version_info >= (3, 0): - return input(prompt) - else: - return str(raw_input(prompt)) + return input(prompt) # Write our prompt sys.stdout.write(prompt) sys.stdout.flush() if os.name == "nt": start_time = time.time() - i = '' + i = "" while True: if msvcrt.kbhit(): c = msvcrt.getche() - if ord(c) == 13: # enter_key + if ord(c) == 13: # enter_key break - elif ord(c) >= 32: #space_char + elif ord(c) >= 32: # space_char i += c if len(i) == 0 and (time.time() - start_time) > timeout: break else: - i, o, e = select.select( [sys.stdin], [], [], timeout ) + i, o, e = select.select([sys.stdin], [], [], timeout) if i: i = sys.stdin.readline().strip() - print('') # needed to move to next line + else: + i = "" + print("") # needed to move to next line if len(i) > 0: return i else: return default def cls(self): - os.system('cls' if os.name=='nt' else 'clear') + os.system("cls" if os.name == "nt" else "clear") def cprint(self, message, **kwargs): strip_colors = kwargs.get("strip_colors", False) if os.name == "nt": strip_colors = True - reset = u"\u001b[0m" + reset = "\u001b[0m" # Requires sys import for c in self.colors: if strip_colors: @@ -188,41 +196,17 @@ def cprint(self, message, **kwargs): sys.stdout.write(message) print(reset) - # Needs work to resize the string if color chars exist - '''# Header drawing method - def head(self, text = None, width = 55): - if text == None: - text = self.name - self.cls() - print(" {}".format("#"*width)) - len_text = self.cprint(text, strip_colors=True) - mid_len = int(round(width/2-len(len_text)/2)-2) - middle = " #{}{}{}#".format(" "*mid_len, len_text, " "*((width - mid_len - len(len_text))-2)) - if len(middle) > width+1: - # Get the difference - di = len(middle) - width - # Add the padding for the ...# - di += 3 - # Trim the string - middle = middle[:-di] - newlen = len(middle) - middle += "...#" - find_list = [ c["find"] for c in self.colors ] - - # Translate colored string to len - middle = middle.replace(len_text, text + self.rt_color) # always reset just in case - self.cprint(middle) - print("#"*width)''' - # Header drawing method - def head(self, text = None, width = 55): - if text == None: + def head(self, text=None, width=55): + if text is None: text = self.name self.cls() - print(" {}".format("#"*width)) - mid_len = int(round(width/2-len(text)/2)-2) - middle = " #{}{}{}#".format(" "*mid_len, text, " "*((width - mid_len - len(text))-2)) - if len(middle) > width+1: + print(" {}".format("#" * width)) + mid_len = int(round(width / 2 - len(text) / 2) - 2) + middle = " #{}{}{}#".format( + " " * mid_len, text, " " * ((width - mid_len - len(text)) - 2) + ) + if len(middle) > width + 1: # Get the difference di = len(middle) - width # Add the padding for the ...# @@ -230,10 +214,10 @@ def head(self, text = None, width = 55): # Trim the string middle = middle[:-di] + "...#" print(middle) - print("#"*width) + print("#" * width) def resize(self, width, height): - print('\033[8;{};{}t'.format(height, width)) + print("\033[8;{};{}t".format(height, width)) def custom_quit(self): self.head() @@ -244,11 +228,11 @@ def custom_quit(self): print("www.github.com/corpnewt\n") # Get the time and wish them a good morning, afternoon, evening, and night hr = datetime.datetime.now().time().hour - if hr > 3 and hr < 12: + if 3 < hr < 12: print("Have a nice morning!\n\n") - elif hr >= 12 and hr < 17: + elif 12 <= hr < 17: print("Have a nice afternoon!\n\n") - elif hr >= 17 and hr < 21: + elif 17 <= hr < 21: print("Have a nice evening!\n\n") else: print("Have a nice night!\n\n")