class App_Button(object): """docstring for App_Button""" def __init__(self, master, text=None, styles=None, command=None, image=None, title=None, disabled=False): super(App_Button, self).__init__() self.master = master self.text = text self.styles = styles self.command = command self.image = image self.title = title self.disabled = disabled self.putButton() def putButton(self): self.btn_wrapper = LabelFrame(self.master, relief=FLAT, borderwidth=0) self.btn = Button(self.btn_wrapper, text=self.text, relief=FLAT, bg=self.styles['btn_bg'], padx=self.styles['padx'], pady=self.styles['pady'], fg=self.styles['btn_fg'], borderwidth=0, font=self.styles['big_font'], command=self.command, image=self.image, activeforeground=self.styles['a_fg'], activebackground=self.styles['a_bg'], cursor="hand2") self.btn.image = self.image if self.disabled: self.btn.bind("<Button-1>", lambda x: "break") else: self.btn.bind("<Enter>", self.mouseover) self.btn.bind("<Leave>", self.mouseout) self.btn.pack() if self.title is not None: self.tooltip = App_Tooltip(self.btn, text=self.title) def mouseover(self, event): self.btn.config(fg=self.styles['h_fg']) self.btn.config(bg=self.styles['h_bg']) def mouseout(self, event): self.btn.config(fg=self.styles['btn_fg']) self.btn.config(bg=self.styles['btn_bg']) def bind(self, *args, **kwargs): self.btn.bind(*args, **kwargs) def bind_wrapper(self, *args, **kwargs): self.btn_wrapper.bind(*args, **kwargs) def pack(self, *args, **kwargs): self.btn_wrapper.pack(*args, **kwargs) def place(self, *args, **kwargs): self.btn_wrapper.place(*args, **kwargs) def config(self, *args, **kwargs): self.btn.config(*args, **kwargs) def set_tooltip(self, text): self.tooltip.configure(text=text) def pack_forget(self): self.btn_wrapper.pack_forget() def place_forget(self): self.btn_wrapper.place_forget() def winfo_rootx(self): return self.btn_wrapper.winfo_rootx() def winfo_rooty(self): return self.btn_wrapper.winfo_rooty() def winfo_height(self): return self.btn_wrapper.winfo_height() def winfo_width(self): return self.btn_wrapper.winfo_width()
class Application(Frame): def __init__(self, master=None): super().__init__(master) self.pack() # First row f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10) # NAND Button self.nand_mode = False nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB' 'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv' 'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7')) self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED) self.nand_button.image = nand_icon self.nand_button.pack(side='left') self.nand_file = StringVar() Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left') Button(f1, text='...', command=self.choose_nand).pack(side='left') f1.pack(padx=10, pady=10, fill=X) # Second row f2 = Frame(self) # Check box self.twilight = IntVar() self.twilight.set(1) self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware', variable=self.twilight) self.chk.pack(padx=10, anchor=W) # NAND operation frame self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10) self.nand_operation = IntVar() self.nand_operation.set(0) Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation, value=0, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation, value=1, command=lambda: self.enable_entries(True)).pack(anchor=W) fl = Frame(self.nand_frame) self.cid_label = Label(fl, text='eMMC CID', state=DISABLED) self.cid_label.pack(anchor=W, padx=(24, 0)) self.cid = StringVar() self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED) self.cid_entry.pack(anchor=W, padx=(24, 0)) fl.pack(side='left') fr = Frame(self.nand_frame) self.console_id_label = Label(fr, text='Console ID', state=DISABLED) self.console_id_label.pack(anchor=W) self.console_id = StringVar() self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED) self.console_id_entry.pack(anchor=W) fr.pack(side='right') f2.pack(fill=X) # Third row f3 = Frame(self) self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED) self.start_button.pack(side='left', padx=(0, 5)) Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0)) f3.pack(pady=(10, 20)) self.folders = [] self.files = [] ################################################################################################ def change_mode(self): if (self.nand_mode): self.nand_frame.pack_forget() self.chk.pack(padx=10, anchor=W) self.nand_mode = False else: if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know ' 'what you are doing. Proceed?'), icon=WARNING): self.chk.pack_forget() self.nand_frame.pack(padx=10, pady=(0, 10), fill=X) self.nand_mode = True ################################################################################################ def enable_entries(self, status): self.cid_label['state'] = (NORMAL if status else DISABLED) self.cid_entry['state'] = (NORMAL if status else DISABLED) self.console_id_label['state'] = (NORMAL if status else DISABLED) self.console_id_entry['state'] = (NORMAL if status else DISABLED) ################################################################################################ def choose_nand(self): name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) )) self.nand_file.set(name) self.nand_button['state'] = (NORMAL if name != '' else DISABLED) self.start_button['state'] = (NORMAL if name != '' else DISABLED) ################################################################################################ def hiya(self): if not self.nand_mode: showinfo('Info', 'Now you will be asked to choose the SD card path that will be used ' 'for installing the custom firmware (or an output folder).\n\nIn order to avoid ' 'boot errors please assure it is empty before continuing.') self.sd_path = askdirectory() # Exit if no path was selected if self.sd_path == '': return # If adding a No$GBA footer, check if CID and ConsoleID values are OK elif self.nand_operation.get() == 1: cid = self.cid.get() console_id = self.console_id.get() # Check lengths if len(cid) != 32: showerror('Error', 'Bad eMMC CID') return elif len(console_id) != 16: showerror('Error', 'Bad Console ID') return # Parse strings to hex try: cid = bytearray.fromhex(cid) except ValueError: showerror('Error', 'Bad eMMC CID') return try: console_id = bytearray(reversed(bytearray.fromhex(console_id))) except ValueError: showerror('Error', 'Bad Console ID') return dialog = Toplevel(self) # Open as dialog (parent disabled) dialog.grab_set() dialog.title('Status') # Disable maximizing dialog.resizable(0, 0) frame = Frame(dialog, bd=2, relief=SUNKEN) scrollbar = Scrollbar(frame) scrollbar.pack(side=RIGHT, fill=Y) self.log = ThreadSafeText(frame, bd=0, width=52, height=20, yscrollcommand=scrollbar.set) self.log.pack() scrollbar.config(command=self.log.yview) frame.pack() Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10) # Center in window dialog.update_idletasks() width = dialog.winfo_width() height = dialog.winfo_height() dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) - (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2))) # Check if we'll be adding a No$GBA footer if self.nand_mode and self.nand_operation.get() == 1: Thread(target=self.add_footer, args=(cid, console_id)).start() else: Thread(target=self.check_nand).start() ################################################################################################ def check_nand(self): self.log.write('Checking NAND file...') # Read the NAND file try: with open(self.nand_file.get(), 'rb') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) if bstr == b'DSi eMMC CID/CPU': # Read the CID bstr = f.read(0x10) self.cid.set(bstr.hex().upper()) self.log.write('- eMMC CID: ' + self.cid.get()) # Read the console ID bstr = f.read(8) self.console_id.set(bytearray(reversed(bstr)).hex().upper()) self.log.write('- Console ID: ' + self.console_id.get()) # Check we are removing the No$GBA footer if self.nand_mode: Thread(target=self.remove_footer).start() else: Thread(target=self.get_latest_hiyacfw).start() else: self.log.write('ERROR: No$GBA footer not found') except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def get_latest_hiyacfw(self): # Try to use already downloaded HiyaCFW archive filename = 'HiyaCFW.7z' try: if path.isfile(filename): self.log.write('\nPreparing HiyaCFW...') else: self.log.write('\nDownloading latest HiyaCFW release...') with urlopen('https://github.com/RocketRobz/hiyaCFW/releases/latest/download/' + filename) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting HiyaCFW archive...') proc = Popen([ _7z, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('for PC') self.folders.append('for SDNAND SD card') # Got to decrypt NAND if bootloader.nds is present Thread(target=self.decrypt_nand if path.isfile('bootloader.nds') else self.extract_bios).start() else: self.log.write('ERROR: Extractor failed') except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get HiyaCFW') except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def extract_bios(self): self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...') try: proc = Popen([ twltool, 'boot2', '--in', self.nand_file.get() ]) ret_val = proc.wait() if ret_val == 0: # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) self.files.append('arm7.bin') self.files.append('arm9.bin') Thread(target=self.patch_bios).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def patch_bios(self): self.log.write('\nPatching ARM7/ARM9 BIOS...') try: self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'), 'arm7.bin') self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'), 'arm9.bin') # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.arm9_prepend).start() except IOError as e: print(e) self.log.write('ERROR: Could not patch BIOS') Thread(target=self.clean, args=(True,)).start() except Exception as e: print(e) self.log.write('ERROR: Invalid patch header') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def arm9_prepend(self): self.log.write('\nPrepending data to ARM9 BIOS...') try: with open('arm9.bin', 'rb') as f: data = f.read() with open('arm9.bin', 'wb') as f: with open(path.join('for PC', 'bootloader files', 'bootloader arm9 append to start.bin'), 'rb') as pre: f.write(pre.read()) f.write(data) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Prepended arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.make_bootloader).start() except IOError as e: print(e) self.log.write('ERROR: Could not prepend data to ARM9 BIOS') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def make_bootloader(self): self.log.write('\nGenerating new bootloader...') exe = (path.join('for PC', 'bootloader files', 'ndstool.exe') if sysname == 'Windows' else path.join(sysname, 'ndsblc')) try: proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t', path.join('for PC', 'bootloader files', 'banner.bin'), '-h', path.join('for PC', 'bootloader files', 'header.bin') ]) ret_val = proc.wait() if ret_val == 0: self.files.append('bootloader.nds') # Hash bootloader.nds sha1_hash = sha1() with open('bootloader.nds', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- bootloader.nds SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.decrypt_nand).start() else: self.log.write('ERROR: Generator failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def decrypt_nand(self): self.log.write('\nDecrypting NAND...') try: proc = Popen([ twltool, 'nandcrypt', '--in', self.nand_file.get(), '--out', self.console_id.get() + '.img' ]) ret_val = proc.wait() print("\n") if ret_val == 0: self.files.append(self.console_id.get() + '.img') Thread(target=self.win_extract_nand if sysname == 'Windows' else self.extract_nand).start() else: self.log.write('ERROR: Decryptor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def win_extract_nand(self): self.log.write('\nExtracting files from NAND...') try: proc = Popen([ _7z, 'x', '-bso0', '-y', self.console_id.get() + '.img', '0.fat' ]) ret_val = proc.wait() if ret_val == 0: self.files.append('0.fat') proc = Popen([ _7z, 'x', '-bso0', '-y', '-o' + self.sd_path, '0.fat' ]) ret_val = proc.wait() if ret_val == 0: Thread(target=self.get_launcher).start() else: self.log.write('ERROR: Extractor failed, please update 7-Zip') Thread(target=self.clean, args=(True,)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def extract_nand(self): self.log.write('\nExtracting files from NAND...') try: # DSi first partition offset: 0010EE00h proc = Popen([ fatcat, '-O', '1109504', '-x', self.sd_path, self.console_id.get() + '.img' ]) ret_val = proc.wait() if ret_val == 0: Thread(target=self.get_launcher).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def get_launcher(self): app = self.detect_region() # Stop if no supported region was found if not app: Thread(target=self.clean, args=(True,)).start() return # Delete contents of the launcher folder as it will be replaced by the one from HiyaCFW launcher_folder = path.join(self.sd_path, 'title', '00030017', app, 'content') # Walk through all files in the launcher content folder for file in listdir(launcher_folder): file = path.join(launcher_folder, file) # Set current file as read/write in case we are in Windows and unlaunch was installed # in the NAND. For Linux and MacOS fatcat doesn't keep file attributes if sysname == 'Windows': chmod(file, 438) # Delete current file remove(file) # Try to use already downloaded launcher try: if path.isfile(self.launcher_region): self.log.write('\nPreparing ' + self.launcher_region + ' launcher...') else: self.log.write('\nDownloading ' + self.launcher_region + ' launcher...') with urlopen('https://raw.githubusercontent.com' '/mondul/HiyaCFW-Helper/master/launchers/' + self.launcher_region) as src, open(self.launcher_region, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Decrypting launcher...') proc = Popen([ _7z, 'x', '-bso0', '-y', '-p' + app, self.launcher_region, '00000002.app' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(self.launcher_region) self.files.append('00000002.app') # Hash 00000002.app sha1_hash = sha1() with open('00000002.app', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched launcher SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.install_hiyacfw, args=(launcher_folder,)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except IOError as e: print(e) self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_hiyacfw(self, launcher_folder): self.log.write('\nCopying HiyaCFW files...') # Reset copied files cache _path_created.clear() copy_tree('for SDNAND SD card', self.sd_path, update=1) copyfile('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds')) copyfile('00000002.app', path.join(launcher_folder, '00000002.app')) Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start() ################################################################################################ def get_latest_twilight(self): # Try to use already downloaded TWiLight Menu++ archive filename = 'TWiLightMenu.7z' try: if path.isfile(filename): self.log.write('\nPreparing TWiLight Menu++...') else: self.log.write('\nDownloading latest TWiLight Menu++ release...') with urlopen('https://github.com/DS-Homebrew/TWiLightMenu/releases/latest/download/' + filename) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting ' + filename[:-3] + ' archive...') proc = Popen([ _7z, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users', 'DSi&3DS - SD card users', 'roms' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('DSi - CFW users') self.folders.append('_nds') self.folders.append('DSi&3DS - SD card users') self.folders.append('roms') Thread(target=self.install_twilight, args=(filename[:-3],)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get TWiLight Menu++') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_twilight(self, name): self.log.write('\nCopying ' + name + ' files...') copy_tree(path.join('DSi - CFW users', 'SDNAND root'), self.sd_path, update=1) copy_tree('_nds', path.join(self.sd_path, '_nds')) copy_tree('DSi&3DS - SD card users', self.sd_path, update=1) copy_tree('roms', path.join(self.sd_path, 'roms')) Thread(target=self.clean).start() ################################################################################################ def clean(self, err=False): self.log.write('\nCleaning...') while len(self.folders) > 0: rmtree(self.folders.pop(), ignore_errors=True) while len(self.files) > 0: try: remove(self.files.pop()) except: pass if err: self.log.write('Done') return self.log.write('Done!\nEject your SD card and insert it into your DSi') ################################################################################################ def patcher(self, patchpath, filepath): patch_size = path.getsize(patchpath) patchfile = open(patchpath, 'rb') if patchfile.read(5) != b'PATCH': patchfile.close() raise Exception() target = open(filepath, 'r+b') # Read First Record r = patchfile.read(3) while patchfile.tell() not in [ patch_size, patch_size - 3 ]: # Unpack 3-byte pointers. offset = self.unpack_int(r) # Read size of data chunk r = patchfile.read(2) size = self.unpack_int(r) if size == 0: # RLE Record r = patchfile.read(2) rle_size = self.unpack_int(r) data = patchfile.read(1) * rle_size else: data = patchfile.read(size) # Write to file target.seek(offset) target.write(data) # Read Next Record r = patchfile.read(3) if patch_size - 3 == patchfile.tell(): trim_size = self.unpack_int(patchfile.read(3)) target.truncate(trim_size) # Cleanup target.close() patchfile.close() ################################################################################################ def unpack_int(self, bstr): # Read an n-byte big-endian integer from a byte string ( ret_val, ) = unpack_from('>I', b'\x00' * (4 - len(bstr)) + bstr) return ret_val ################################################################################################ def detect_region(self): REGION_CODES = { '484e4145': 'USA', '484e414a': 'JAP', '484e414b': 'KOR', '484e4150': 'EUR', '484e4155': 'AUS' } # Autodetect console region try: for app in listdir(path.join(self.sd_path, 'title', '00030017')): for file in listdir(path.join(self.sd_path, 'title', '00030017', app, 'content')): if file.endswith('.app'): try: self.log.write('- Detected ' + REGION_CODES[app] + ' console NAND dump') self.launcher_region = REGION_CODES[app] return app except KeyError: self.log.write('ERROR: Unsupported console region') return False self.log.write('ERROR: Could not detect console region') except OSError as e: self.log.write('ERROR: ' + e.strerror + ': ' + e.filename) return False ################################################################################################ def remove_footer(self): self.log.write('\nRemoving No$GBA footer...') file = self.console_id.get() + '-no-footer.bin' try: copyfile(self.nand_file.get(), file) # Back-up footer info with open(self.console_id.get() + '-info.txt', 'w') as f: f.write('eMMC CID: ' + self.cid.get() + '\r\n') f.write('Console ID: ' + self.console_id.get() + '\r\n') with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Remove footer f.truncate() self.log.write('\nDone!\nModified NAND stored as\n' + file + '\nStored footer info in ' + self.console_id.get() + '-info.txt') except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def add_footer(self, cid, console_id): self.log.write('Adding No$GBA footer...') file = self.console_id.get() + '-footer.bin' try: copyfile(self.nand_file.get(), file) with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) # Check if it already has a footer if bstr == b'DSi eMMC CID/CPU': self.log.write('ERROR: File already has a No$GBA footer') f.close() remove(file) return # Go to the end of file f.seek(0, 2) # Write footer f.write(b'DSi eMMC CID/CPU') f.write(cid) f.write(console_id) f.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') self.log.write('\nDone!\nModified NAND stored as\n' + file) except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get()))
class ConfigurationWindow: clusterName = "CLUSTER_NAME" zookeeperHost = "ZOOKEEPER_HOST" zookeeperPort = "ZOOKEEPER_PORT" def __init__(self): self.cluster_name = None self.zookeeper_host = None self.zookeeper_port = None self.kafka_window = None self.frame = Frame() self.frame.config(padx=10, pady=10) self.frame.grid(row=0, column=0, sticky="wn") self.cluster_frame = LabelFrame(self.frame, text="Clusters") self.cluster_frame.config(font=("Arial", 14), padx=10, pady=10) self.cluster_frame.grid(row=1, column=0) self.list_clusters_frame() def save(self, old_cluster_name): if (old_cluster_name == ""): insert_configuration(self.cluster_name.get(), self.zookeeper_host.get(), self.zookeeper_port.get()) else: update_configuration(self.cluster_name.get(), self.zookeeper_host.get(), self.zookeeper_port.get(), old_cluster_name) self.clean_frame(self.cluster_frame) self.list_clusters_frame() def delete(self, cluster_name): delete_configuration(cluster_name) self.clean_frame(self.cluster_frame) self.list_clusters_frame() def disconnect(self): self.clean_frame(self.cluster_frame) self.list_clusters_frame() self.kafka_window.destroy() def connect(self, cluster_name): self.clean_frame(self.cluster_frame) self.disconnect_cluster_frame(cluster_name) configuration = get_cluster(cluster_name) self.kafka_window = topic_list_window.KafkaWindow() self.kafka_window.open_window(configuration[0][1], configuration[0][2]) def settings(self, new_cluster_name=None): self.clean_frame(self.cluster_frame) cluster_name = "" cluster_host = "" cluster_port = "" if (new_cluster_name != None): configuration = get_cluster(new_cluster_name) cluster_name = configuration[0][0] cluster_host = configuration[0][1] cluster_port = configuration[0][2] # Cluster Name Fields Label(self.cluster_frame, text="Cluster Name").grid(row=0, column=0) self.cluster_name = Entry(self.cluster_frame, width=40) self.cluster_name.grid(row=0, column=1) self.cluster_name.insert('0', cluster_name) # Zookeeper Host Fields Label(self.cluster_frame, text="Zookeeper Host").grid(row=1, column=0) self.zookeeper_host = Entry(self.cluster_frame, width=40) self.zookeeper_host.grid(row=1, column=1) self.zookeeper_host.insert('0', cluster_host) # Zookeeper Port Fields Label(self.cluster_frame, text="Zookeeper Port").grid(row=2, column=0) self.zookeeper_port = Entry(self.cluster_frame, width=40) self.zookeeper_port.grid(row=2, column=1) self.zookeeper_port.insert('0', cluster_port) # Button Group Button(self.cluster_frame, text="Save & Close", command=lambda: self.save(cluster_name)).grid(row=3, column=1) def list_clusters_frame(self): configurations = get_all_clusters() connect_image = PhotoImage(file="resources/images/connect.gif") settings_image = PhotoImage(file="resources/images/settings.gif") delete_image = PhotoImage(file="resources/images/delete.gif") counter = 0 for configuration in configurations: configuration_cluster_name = configuration[0] cluster_name_label = Label(self.cluster_frame, text=configuration_cluster_name) cluster_name_label.config(font=("Arial", 12)) cluster_name_label.grid(row=counter, column=1) connect_button = Button(self.cluster_frame, text="connect", image=connect_image, command=partial( self.connect, configuration_cluster_name)) connect_button.image = connect_image connect_button.grid(row=counter, column=2) settings_button = Button(self.cluster_frame, text="settings", image=settings_image, command=partial( self.settings, configuration_cluster_name)) settings_button.image = settings_image settings_button.grid(row=counter, column=3) delete_button = Button(self.cluster_frame, text="delete", image=delete_image, command=partial(self.delete, configuration_cluster_name)) delete_button.image = delete_image delete_button.grid(row=counter, column=4) counter += 1 add_image = PhotoImage(file="resources/images/add.gif") add_button = Button(self.cluster_frame, text="add", image=add_image, command=lambda: self.settings()) add_button.image = add_image add_button.grid(row=counter, column=4, pady=10) def disconnect_cluster_frame(self, cluster_name): self.cluster_frame.pack_forget() disconnect_image = PhotoImage(file="resources/images/disconnect.gif") cluster_name_label = Label(self.cluster_frame, text=cluster_name) cluster_name_label.config(font=("Arial", 12)) cluster_name_label.grid(row=0, column=1) connect_button = Button(self.cluster_frame, text="disconnect", image=disconnect_image, command=lambda: self.disconnect()) connect_button.image = disconnect_image connect_button.grid(row=0, column=2) @staticmethod def clean_frame(frame): for widget in frame.winfo_children(): widget.destroy()
class Application(Frame): def __init__(self, master=None): super().__init__(master) self.pack() # First row f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10) # NAND Button self.nand_mode = False nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB' 'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv' 'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7')) self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED) self.nand_button.image = nand_icon self.nand_button.pack(side='left') self.nand_file = StringVar() Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left') Button(f1, text='...', command=self.choose_nand).pack(side='left') f1.pack(padx=10, pady=10, fill=X) # Second row f2 = Frame(self) # Check box self.twilight = IntVar() self.twilight.set(1) self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware', variable=self.twilight) self.chk.pack(padx=10, anchor=W) # NAND operation frame self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10) self.nand_operation = IntVar() self.nand_operation.set(0) Radiobutton(self.nand_frame, text='Uninstall unlaunch or install v1.4 stable', variable=self.nand_operation, value=0, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation, value=1, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation, value=2, command=lambda: self.enable_entries(True)).pack(anchor=W) fl = Frame(self.nand_frame) self.cid_label = Label(fl, text='eMMC CID', state=DISABLED) self.cid_label.pack(anchor=W, padx=(24, 0)) self.cid = StringVar() self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED) self.cid_entry.pack(anchor=W, padx=(24, 0)) fl.pack(side='left') fr = Frame(self.nand_frame) self.console_id_label = Label(fr, text='Console ID', state=DISABLED) self.console_id_label.pack(anchor=W) self.console_id = StringVar() self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED) self.console_id_entry.pack(anchor=W) fr.pack(side='right') f2.pack(fill=X) # Third row f3 = Frame(self) self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED) self.start_button.pack(side='left', padx=(0, 5)) Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0)) f3.pack(pady=(10, 20)) self.folders = [] self.files = [] ################################################################################################ def change_mode(self): if (self.nand_mode): self.nand_frame.pack_forget() self.chk.pack(padx=10, anchor=W) self.nand_mode = False else: if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know ' 'what you are doing. Proceed?'), icon=WARNING): self.chk.pack_forget() self.nand_frame.pack(padx=10, pady=(0, 10), fill=X) self.nand_mode = True ################################################################################################ def enable_entries(self, status): self.cid_label['state'] = (NORMAL if status else DISABLED) self.cid_entry['state'] = (NORMAL if status else DISABLED) self.console_id_label['state'] = (NORMAL if status else DISABLED) self.console_id_entry['state'] = (NORMAL if status else DISABLED) ################################################################################################ def choose_nand(self): name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) )) self.nand_file.set(name) self.nand_button['state'] = (NORMAL if name != '' else DISABLED) self.start_button['state'] = (NORMAL if name != '' else DISABLED) ################################################################################################ def hiya(self): if not self.nand_mode: showinfo('Info', 'Now you will be asked to choose the SD card path that will be used ' 'for installing the custom firmware (or an output folder).\n\nIn order to avoid ' 'boot errors please assure it is empty before continuing.') self.sd_path = askdirectory() # Exit if no path was selected if self.sd_path == '': return # If adding a No$GBA footer, check if CID and ConsoleID values are OK elif self.nand_operation.get() == 2: cid = self.cid.get() console_id = self.console_id.get() # Check lengths if len(cid) != 32: showerror('Error', 'Bad eMMC CID') return elif len(console_id) != 16: showerror('Error', 'Bad Console ID') return # Parse strings to hex try: cid = bytearray.fromhex(cid) except ValueError: showerror('Error', 'Bad eMMC CID') return try: console_id = bytearray(reversed(bytearray.fromhex(console_id))) except ValueError: showerror('Error', 'Bad Console ID') return dialog = Toplevel(self) # Open as dialog (parent disabled) dialog.grab_set() dialog.title('Status') # Disable maximizing dialog.resizable(0, 0) frame = Frame(dialog, bd=2, relief=SUNKEN) scrollbar = Scrollbar(frame) scrollbar.pack(side=RIGHT, fill=Y) self.log = ThreadSafeText(frame, bd=0, width=52, height=20, yscrollcommand=scrollbar.set) self.log.pack() scrollbar.config(command=self.log.yview) frame.pack() Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10) # Center in window dialog.update_idletasks() width = dialog.winfo_width() height = dialog.winfo_height() dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) - (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2))) # Check if we'll be adding a No$GBA footer if self.nand_mode and self.nand_operation.get() == 2: Thread(target=self.add_footer, args=(cid, console_id)).start() else: Thread(target=self.check_nand).start() ################################################################################################ def check_nand(self): self.log.write('Checking NAND file...') # Read the NAND file try: with open(self.nand_file.get(), 'rb') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) if bstr == b'DSi eMMC CID/CPU': # Read the CID bstr = f.read(0x10) self.cid.set(bstr.hex().upper()) self.log.write('- eMMC CID: ' + self.cid.get()) # Read the console ID bstr = f.read(8) self.console_id.set(bytearray(reversed(bstr)).hex().upper()) self.log.write('- Console ID: ' + self.console_id.get()) # Check we are making an unlaunch operation or removing the No$GBA footer if self.nand_mode: if self.nand_operation.get() == 0: Thread(target=self.decrypt_nand).start() else: Thread(target=self.remove_footer).start() pass else: Thread(target=self.get_latest_hiyacfw).start() else: self.log.write('ERROR: No$GBA footer not found') except IOError as e: print(e) self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def get_latest_hiyacfw(self): # Try to use already downloaded HiyaCFW archive filename = 'HiyaCFW.7z' try: if path.isfile(filename): self.log.write('\nPreparing HiyaCFW...') else: self.log.write('\nDownloading latest HiyaCFW release...') conn = urlopen('https://api.github.com/repos/RocketRobz/hiyaCFW/releases/latest') latest = jsonify(conn) conn.close() with urlopen(latest['assets'][0] ['browser_download_url']) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting HiyaCFW archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('for PC') self.folders.append('for SDNAND SD card') # Got to decrypt NAND if bootloader.nds is present Thread(target=self.decrypt_nand if path.isfile('bootloader.nds') else self.extract_bios).start() else: self.log.write('ERROR: Extractor failed') except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get HiyaCFW') except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def extract_bios(self): self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'boot2', '--in', self.nand_file.get() ]) ret_val = proc.wait() if ret_val == 0: # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) self.files.append('arm7.bin') self.files.append('arm9.bin') Thread(target=self.patch_bios).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def patch_bios(self): self.log.write('\nPatching ARM7/ARM9 BIOS...') try: self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'), 'arm7.bin') self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'), 'arm9.bin') # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.arm9_prepend).start() except IOError as e: print(e) self.log.write('ERROR: Could not patch BIOS') Thread(target=self.clean, args=(True,)).start() except Exception as e: print(e) self.log.write('ERROR: Invalid patch header') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def arm9_prepend(self): self.log.write('\nPrepending data to ARM9 BIOS...') try: with open('arm9.bin', 'rb') as f: data = f.read() with open('arm9.bin', 'wb') as f: with open(path.join('for PC', 'bootloader files', 'bootloader arm9 append to start.bin'), 'rb') as pre: f.write(pre.read()) f.write(data) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Prepended arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.make_bootloader).start() except IOError as e: print(e) self.log.write('ERROR: Could not prepend data to ARM9 BIOS') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def make_bootloader(self): self.log.write('\nGenerating new bootloader...') exe = (path.join('for PC', 'bootloader files', 'ndstool') if sysname == 'Windows' else path.join(sysname, 'ndsblc')) try: proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t', path.join('for PC', 'bootloader files', 'banner.bin'), '-h', path.join('for PC', 'bootloader files', 'header.bin') ]) ret_val = proc.wait() if ret_val == 0: # Hash bootloader.nds sha1_hash = sha1() with open('bootloader.nds', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- bootloader.nds SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.decrypt_nand).start() else: self.log.write('ERROR: Generator failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def decrypt_nand(self): self.log.write('\nDecrypting NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'nandcrypt', '--in', self.nand_file.get(), '--out', self.console_id.get() + '.img' ]) ret_val = proc.wait() print("\n") if ret_val == 0: if not self.nand_mode: self.files.append(self.console_id.get() + '.img') Thread(target=self.mount_nand).start() else: self.log.write('ERROR: Decryptor failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def mount_nand(self): self.log.write('\nMounting decrypted NAND...') try: if sysname == 'Windows': exe = osfmount cmd = [ osfmount, '-a', '-t', 'file', '-f', self.console_id.get() + '.img', '-m', '#:', '-o', 'ro,rem' ] if self.nand_mode: cmd[-1] = 'rw,rem' proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'[a-zA-Z]:\s', outs.decode('utf-8')).group(0).strip() self.log.write('- Mounted on drive ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return elif sysname == 'Darwin': exe = 'hdiutil' cmd = [ exe, 'attach', '-imagekey', 'diskimage-class=CRawDiskImage', '-nomount', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.raw_disk = search(r'^\/dev\/disk\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted raw disk on ' + self.raw_disk) cmd = [ exe, 'mount', self.raw_disk + 's1' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'\/Volumes\/.+', outs.decode('utf-8')).group(0) self.log.write('- Mounted volume on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: # Linux exe = 'losetup' cmd = [ exe, '-P', '-f', '--show', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-r') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.loop_dev = search(r'\/dev\/loop\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted loop device on ' + self.loop_dev) exe = 'mount' self.mounted = '/mnt' cmd = [ exe, '-t', 'vfat', self.loop_dev + 'p1', self.mounted ] if not self.nand_mode: cmd.insert(1, '-r') proc = Popen(cmd) ret_val = proc.wait() if ret_val == 0: self.log.write('- Mounted partition on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return # Check we are making an unlaunch operation Thread(target=self.unlaunch_proc if self.nand_mode else self.extract_nand).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def extract_nand(self): self.log.write('\nExtracting files from NAND...') err = False # Reset copied files cache _path_created.clear() try: copy_tree(self.mounted, self.sd_path, preserve_mode=0, update=1) except Exception as e: print(e) self.log.write('ERROR: Extractor failed') err = True Thread(target=self.unmount_nand, args=(err,)).start() ################################################################################################ def unmount_nand(self, err=False): self.log.write('\nUnmounting NAND...') try: if sysname == 'Windows': exe = osfmount proc = Popen([ osfmount, '-D', '-m', self.mounted ]) elif sysname == 'Darwin': exe = 'hdiutil' proc = Popen([ exe, 'detach', self.raw_disk ]) else: # Linux exe = 'umount' proc = Popen([ exe, self.mounted ]) ret_val = proc.wait() if ret_val == 0: exe = 'losetup' proc = Popen([ exe, '-d', self.loop_dev ]) else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() return ret_val = proc.wait() if ret_val == 0: if err: Thread(target=self.clean, args=(True,)).start() else: Thread(target=self.encrypt_nand if self.nand_mode else self.get_launcher).start() else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def get_launcher(self): app = self.detect_region() # Stop if no supported region was found if not app: self.files.append('bootloader.nds') Thread(target=self.clean, args=(True,)).start() return # Check if unlaunch was installed on the NAND dump tmd = path.join(self.sd_path, 'title', '00030017', app, 'content', 'title.tmd') if path.getsize(tmd) > 520: self.log.write('- WARNING: Unlaunch installed on the NAND dump') # Delete title.tmd in case it does not get overwritten remove(tmd) # Try to use already downloaded launcher try: if path.isfile(self.launcher_region): self.log.write('\nPreparing ' + self.launcher_region + ' launcher...') else: self.log.write('\nDownloading ' + self.launcher_region + ' launcher...') with urlopen('https://raw.githubusercontent.com' '/mondul/HiyaCFW-Helper/master/launchers/' + self.launcher_region) as src, open(self.launcher_region, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Decrypting launcher...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', '-p' + app, self.launcher_region, '00000002.app' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(self.launcher_region) # Hash 00000002.app sha1_hash = sha1() with open('00000002.app', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched launcher SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.install_hiyacfw, args=(path.join(self.sd_path, 'title', '00030017', app, 'content', '00000002.app'),)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except IOError as e: print(e) self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher') Thread(target=self.clean, args=(True,)).start() except OSError as e: print(e) self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_hiyacfw(self, launcher_path): self.log.write('\nCopying HiyaCFW files...') copy_tree('for SDNAND SD card', self.sd_path, update=1) move('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds')) move('00000002.app', launcher_path) Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start() ################################################################################################ def get_latest_twilight(self): filename = 'TWiLightMenu.7z' try: self.log.write('\nDownloading latest TWiLight Menu++ release...') conn = urlopen('https://api.github.com/repos/DS-Homebrew/TWiLightMenu/releases/' 'latest') latest = jsonify(conn) conn.close() with urlopen(latest['assets'][0] ['browser_download_url']) as src, open(filename, 'wb') as dst: copyfileobj(src, dst) self.log.write('- Extracting ' + filename[:-3] + ' archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users', 'DSi&3DS - SD card users', 'roms' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('DSi - CFW users') self.folders.append('DSi&3DS - SD card users') Thread(target=self.install_twilight, args=(filename[:-3],)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except (URLError, IOError) as e: print(e) self.log.write('ERROR: Could not get TWiLight Menu++') Thread(target=self.clean, args=(True,)).start()
class Application(Frame): def __init__(self, master=None): super().__init__(master) self.pack() # First row f1 = LabelFrame(self, text='NAND file with No$GBA footer', padx=10, pady=10) # NAND Button self.nand_mode = False nand_icon = PhotoImage(data=('R0lGODlhEAAQAIMAAAAAADMzM2ZmZpmZmczMzP///wAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAMAAAYALAAAAAAQAB' 'AAAARG0MhJaxU4Y2sECAEgikE1CAFRhGMwSMJwBsU6frIgnR/bv' 'hTPrWUSDnGw3JGU2xmHrsvyU5xGO8ql6+S0AifPW8kCKpcpEQA7')) self.nand_button = Button(f1, image=nand_icon, command=self.change_mode, state=DISABLED) self.nand_button.image = nand_icon self.nand_button.pack(side='left') self.nand_file = StringVar() Entry(f1, textvariable=self.nand_file, state='readonly', width=40).pack(side='left') Button(f1, text='...', command=self.choose_nand).pack(side='left') f1.pack(padx=10, pady=10, fill=X) # Second row f2 = Frame(self) # Check box self.twilight = IntVar() self.twilight.set(1) self.chk = Checkbutton(f2, text='Install latest TWiLight Menu++ on custom firmware', variable=self.twilight) self.chk.pack(padx=10, anchor=W) # NAND operation frame self.nand_frame = LabelFrame(f2, text='NAND operation', padx=10, pady=10) self.nand_operation = IntVar() self.nand_operation.set(0) Radiobutton(self.nand_frame, text='Uninstall unlaunch or install v1.4 stable', variable=self.nand_operation, value=0, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Remove No$GBA footer', variable=self.nand_operation, value=1, command=lambda: self.enable_entries(False)).pack(anchor=W) Radiobutton(self.nand_frame, text='Add No$GBA footer', variable=self.nand_operation, value=2, command=lambda: self.enable_entries(True)).pack(anchor=W) fl = Frame(self.nand_frame) self.cid_label = Label(fl, text='eMMC CID', state=DISABLED) self.cid_label.pack(anchor=W, padx=(24, 0)) self.cid = StringVar() self.cid_entry = Entry(fl, textvariable=self.cid, width=20, state=DISABLED) self.cid_entry.pack(anchor=W, padx=(24, 0)) fl.pack(side='left') fr = Frame(self.nand_frame) self.console_id_label = Label(fr, text='Console ID', state=DISABLED) self.console_id_label.pack(anchor=W) self.console_id = StringVar() self.console_id_entry = Entry(fr, textvariable=self.console_id, width=20, state=DISABLED) self.console_id_entry.pack(anchor=W) fr.pack(side='right') f2.pack(fill=X) # Third row f3 = Frame(self) self.start_button = Button(f3, text='Start', width=16, command=self.hiya, state=DISABLED) self.start_button.pack(side='left', padx=(0, 5)) Button(f3, text='Quit', command=root.destroy, width=16).pack(side='left', padx=(5, 0)) f3.pack(pady=(10, 20)) self.folders = [] self.files = [] ################################################################################################ def change_mode(self): if (self.nand_mode): self.nand_frame.pack_forget() self.chk.pack(padx=10, anchor=W) self.nand_mode = False else: if askokcancel('Warning', ('You are about to enter NAND mode. Do it only if you know ' 'what you are doing. Proceed?'), icon=WARNING): self.chk.pack_forget() self.nand_frame.pack(padx=10, pady=(0, 10), fill=X) self.nand_mode = True ################################################################################################ def enable_entries(self, status): self.cid_label['state'] = (NORMAL if status else DISABLED) self.cid_entry['state'] = (NORMAL if status else DISABLED) self.console_id_label['state'] = (NORMAL if status else DISABLED) self.console_id_entry['state'] = (NORMAL if status else DISABLED) ################################################################################################ def choose_nand(self): name = askopenfilename(filetypes=( ( 'nand.bin', '*.bin' ), ( 'DSi-1.mmc', '*.mmc' ) )) self.nand_file.set(name) self.nand_button['state'] = (NORMAL if name != '' else DISABLED) self.start_button['state'] = (NORMAL if name != '' else DISABLED) ################################################################################################ def hiya(self): if not self.nand_mode: showinfo('Info', 'Now you will be asked to choose the SD card path that will be used ' 'for installing the custom firmware (or an output folder).\n\nIn order to avoid ' 'boot errors please assure it is empty before continuing.') self.sd_path = askdirectory() # Exit if no path was selected if self.sd_path == '': return # If adding a No$GBA footer, check if CID and ConsoleID values are OK elif self.nand_operation.get() == 2: cid = self.cid.get() console_id = self.console_id.get() # Check lengths if len(cid) != 32: showerror('Error', 'Bad eMMC CID') return elif len(console_id) != 16: showerror('Error', 'Bad Console ID') return # Parse strings to hex try: cid = cid.decode('hex') except TypeError: showerror('Error', 'Bad eMMC CID') return try: console_id = bytearray(reversed(console_id.decode('hex'))) except TypeError: showerror('Error', 'Bad Console ID') return dialog = Toplevel(self) # Open as dialog (parent disabled) dialog.grab_set() dialog.title('Status') # Disable maximizing dialog.resizable(0, 0) frame = Frame(dialog, bd=2, relief=SUNKEN) scrollbar = Scrollbar(frame) scrollbar.pack(side=RIGHT, fill=Y) self.log = ThreadSafeText(frame, bd=0, width=52, height=20, yscrollcommand=scrollbar.set) self.log.pack() scrollbar.config(command=self.log.yview) frame.pack() Button(dialog, text='Close', command=dialog.destroy, width=16).pack(pady=10) # Center in window dialog.update_idletasks() width = dialog.winfo_width() height = dialog.winfo_height() dialog.geometry('%dx%d+%d+%d' % (width, height, root.winfo_x() + (root.winfo_width() / 2) - (width / 2), root.winfo_y() + (root.winfo_height() / 2) - (height / 2))) # Check if we'll be adding a No$GBA footer if self.nand_mode and self.nand_operation.get() == 2: Thread(target=self.add_footer, args=(cid, console_id)).start() else: Thread(target=self.check_nand).start() ################################################################################################ def check_nand(self): self.log.write('Checking NAND file...') # Read the NAND file try: with open(self.nand_file.get(), 'rb') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) if bstr == b'DSi eMMC CID/CPU': # Read the CID bstr = f.read(0x10) self.cid.set(bstr.hex().upper()) self.log.write('- eMMC CID: ' + self.cid.get()) # Read the console ID bstr = f.read(8) self.console_id.set(bytearray(reversed(bstr)).hex().upper()) self.log.write('- Console ID: ' + self.console_id.get()) # Check we are making an unlaunch operation or removing the No$GBA footer if self.nand_mode: if self.nand_operation.get() == 0: Thread(target=self.decrypt_nand).start() else: Thread(target=self.remove_footer).start() pass else: Thread(target=self.get_latest_hiyacfw).start() else: self.log.write('ERROR: No$GBA footer not found') except IOError: self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def get_latest_hiyacfw(self): filename = 'HiyaCFW.7z' try: self.log.write('\nDownloading latest HiyaCFW release...') conn = urlopen('https://api.github.com/repos/RocketRobz/hiyaCFW/releases/latest') latest = jsonify(conn) conn.close() urlretrieve(latest['assets'][0]['browser_download_url'], filename) self.log.write('- Extracting HiyaCFW archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'for PC', 'for SDNAND SD card' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('for PC') self.folders.append('for SDNAND SD card') # Got to decrypt NAND if bootloader.nds is present Thread(target=self.decrypt_nand if path.isfile('bootloader.nds') else self.extract_bios).start() else: self.log.write('ERROR: Extractor failed') except (URLError, IOError) as e: self.log.write('ERROR: Could not get HiyaCFW') except OSError: self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def extract_bios(self): self.log.write('\nExtracting ARM7/ARM9 BIOS from NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'boot2', '--in', self.nand_file.get() ]) ret_val = proc.wait() if ret_val == 0: # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) self.files.append('arm7.bin') self.files.append('arm9.bin') Thread(target=self.patch_bios).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def patch_bios(self): self.log.write('\nPatching ARM7/ARM9 BIOS...') try: self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm7 patch.ips'), 'arm7.bin') self.patcher(path.join('for PC', 'bootloader files', 'bootloader arm9 patch.ips'), 'arm9.bin') # Hash arm7.bin sha1_hash = sha1() with open('arm7.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm7.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.arm9_prepend).start() except IOError: self.log.write('ERROR: Could not patch BIOS') Thread(target=self.clean, args=(True,)).start() except Exception: self.log.write('ERROR: Invalid patch header') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def arm9_prepend(self): self.log.write('\nPrepending data to ARM9 BIOS...') try: with open('arm9.bin', 'rb') as f: data = f.read() with open('arm9.bin', 'wb') as f: with open(path.join('for PC', 'bootloader files', 'bootloader arm9 append to start.bin'), 'rb') as pre: f.write(pre.read()) f.write(data) # Hash arm9.bin sha1_hash = sha1() with open('arm9.bin', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Prepended arm9.bin SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.make_bootloader).start() except IOError: self.log.write('ERROR: Could not prepend data to ARM9 BIOS') Thread(target=self.clean, args=(True,)).start() ################################################################################################ def make_bootloader(self): self.log.write('\nGenerating new bootloader...') exe = (path.join('for PC', 'bootloader files', 'ndstool') if sysname == 'Windows' else path.join(sysname, 'ndsblc')) try: proc = Popen([ exe, '-c', 'bootloader.nds', '-9', 'arm9.bin', '-7', 'arm7.bin', '-t', path.join('for PC', 'bootloader files', 'banner.bin'), '-h', path.join('for PC', 'bootloader files', 'header.bin') ]) ret_val = proc.wait() if ret_val == 0: # Hash bootloader.nds sha1_hash = sha1() with open('bootloader.nds', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- bootloader.nds SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.decrypt_nand).start() else: self.log.write('ERROR: Generator failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def decrypt_nand(self): self.log.write('\nDecrypting NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'nandcrypt', '--in', self.nand_file.get(), '--out', self.console_id.get() + '.img' ]) ret_val = proc.wait() if ret_val == 0: if not self.nand_mode: self.files.append(self.console_id.get() + '.img') Thread(target=self.mount_nand).start() else: self.log.write('ERROR: Decryptor failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def mount_nand(self): self.log.write('\nMounting decrypted NAND...') try: if sysname == 'Windows': exe = osfmount cmd = [ osfmount, '-a', '-t', 'file', '-f', self.console_id.get() + '.img', '-m', '#:', '-o', 'ro,rem' ] if self.nand_mode: cmd[-1] = 'rw,rem' proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'[a-zA-Z]:\s', outs.decode('utf-8')).group(0).strip() self.log.write('- Mounted on drive ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return elif sysname == 'Darwin': exe = 'hdiutil' cmd = [ exe, 'attach', '-imagekey', 'diskimage-class=CRawDiskImage', '-nomount', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.raw_disk = search(r'^\/dev\/disk\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted raw disk on ' + self.raw_disk) cmd = [ exe, 'mount', self.raw_disk + 's1' ] if not self.nand_mode: cmd.insert(2, '-readonly') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.mounted = search(r'\/Volumes\/.+', outs.decode('utf-8')).group(0) self.log.write('- Mounted volume on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: # Linux exe = 'losetup' cmd = [ exe, '-P', '-f', '--show', self.console_id.get() + '.img' ] if not self.nand_mode: cmd.insert(2, '-r') proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) outs, errs = proc.communicate() if proc.returncode == 0: self.loop_dev = search(r'\/dev\/loop\d+', outs.decode('utf-8')).group(0) self.log.write('- Mounted loop device on ' + self.loop_dev) exe = 'mount' self.mounted = '/mnt' cmd = [ exe, '-t', 'vfat', self.loop_dev + 'p1', self.mounted ] if not self.nand_mode: cmd.insert(1, '-r') proc = Popen(cmd) ret_val = proc.wait() if ret_val == 0: self.log.write('- Mounted partition on ' + self.mounted) else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return else: self.log.write('ERROR: Mounter failed') Thread(target=self.clean, args=(True,)).start() return # Check we are making an unlaunch operation Thread(target=self.unlaunch_proc if self.nand_mode else self.extract_nand).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def extract_nand(self): self.log.write('\nExtracting files from NAND...') err = False # Reset copied files cache _path_created.clear() try: copy_tree(self.mounted, self.sd_path, preserve_mode=0, update=1) except: self.log.write('ERROR: Extractor failed') err = True Thread(target=self.unmount_nand, args=(err,)).start() ################################################################################################ def unmount_nand(self, err=False): self.log.write('\nUnmounting NAND...') try: if sysname == 'Windows': exe = osfmount proc = Popen([ osfmount, '-D', '-m', self.mounted ]) elif sysname == 'Darwin': exe = 'hdiutil' proc = Popen([ exe, 'detach', self.raw_disk ]) else: # Linux exe = 'umount' proc = Popen([ exe, self.mounted ]) ret_val = proc.wait() if ret_val == 0: exe = 'losetup' proc = Popen([ exe, '-d', self.loop_dev ]) else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() return ret_val = proc.wait() if ret_val == 0: if err: Thread(target=self.clean, args=(True,)).start() else: Thread(target=self.encrypt_nand if self.nand_mode else self.get_launcher).start() else: self.log.write('ERROR: Unmounter failed') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def get_launcher(self): app = self.detect_region() # Stop if no supported region was found if not app: self.files.append('bootloader.nds') Thread(target=self.clean, args=(True,)).start() return # Check if unlaunch was installed on the NAND dump tmd = path.join(self.sd_path, 'title', '00030017', app, 'content', 'title.tmd') if path.getsize(tmd) > 520: self.log.write('- WARNING: Unlaunch installed on the NAND dump') # Delete title.tmd in case it does not get overwritten remove(tmd) try: self.log.write('\nDownloading ' + self.launcher_region + ' launcher...') urlretrieve('https://raw.githubusercontent.com/mondul/HiyaCFW-Helper/master/' 'launchers/' + self.launcher_region, self.launcher_region) self.log.write('- Decrypting launcher...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', '-p' + app, self.launcher_region, '00000002.app' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(self.launcher_region) # Hash 00000002.app sha1_hash = sha1() with open('00000002.app', 'rb') as f: sha1_hash.update(f.read()) self.log.write('- Patched launcher SHA1:\n ' + sha1_hash.digest().hex().upper()) Thread(target=self.install_hiyacfw, args=(path.join(self.sd_path, 'title', '00030017', app, 'content', '00000002.app'),)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except IOError: self.log.write('ERROR: Could not download ' + self.launcher_region + ' launcher') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_hiyacfw(self, launcher_path): self.log.write('\nCopying HiyaCFW files...') copy_tree('for SDNAND SD card', self.sd_path, update=1) move('bootloader.nds', path.join(self.sd_path, 'hiya', 'bootloader.nds')) move('00000002.app', launcher_path) Thread(target=self.get_latest_twilight if self.twilight.get() == 1 else self.clean).start() ################################################################################################ def get_latest_twilight(self): filename = 'TWiLightMenu.7z' try: self.log.write('\nDownloading latest TWiLight Menu++ release...') conn = urlopen('https://api.github.com/repos/RocketRobz/TWiLightMenu/releases/' 'latest') latest = jsonify(conn) conn.close() urlretrieve(latest['assets'][0]['browser_download_url'], filename) self.log.write('- Extracting ' + filename[:-3] + ' archive...') exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, '_nds', 'DSi - CFW users', 'DSi&3DS - SD card users', 'roms' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.folders.append('DSi - CFW users') self.folders.append('DSi&3DS - SD card users') Thread(target=self.install_twilight, args=(filename[:-3],)).start() else: self.log.write('ERROR: Extractor failed') Thread(target=self.clean, args=(True,)).start() except (URLError, IOError) as e: self.log.write('ERROR: Could not get TWiLight Menu++') Thread(target=self.clean, args=(True,)).start() except OSError: self.log.write('ERROR: Could not execute ' + exe) Thread(target=self.clean, args=(True,)).start() ################################################################################################ def install_twilight(self, name): self.log.write('\nCopying ' + name + ' files...') copy_tree(path.join('DSi - CFW users', 'SDNAND root'), self.sd_path, update=1) move('_nds', path.join(self.sd_path, '_nds')) copy_tree('DSi&3DS - SD card users', self.sd_path, update=1) move('roms', path.join(self.sd_path, 'roms')) # Set files as read-only twlcfg0 = path.join(self.sd_path, 'shared1', 'TWLCFG0.dat') twlcfg1 = path.join(self.sd_path, 'shared1', 'TWLCFG1.dat') if sysname == 'Darwin': Popen([ 'chflags', 'uchg', twlcfg0, twlcfg1 ]).wait() elif sysname == 'Linux': Popen([ path.join('Linux', 'fatattr'), '+R', twlcfg0, twlcfg1 ]).wait() else: chmod(twlcfg0, 292) chmod(twlcfg1, 292) Thread(target=self.clean).start() ################################################################################################ def clean(self, err=False): self.log.write('\nCleaning...') while len(self.folders) > 0: rmtree(self.folders.pop(), ignore_errors=True) while len(self.files) > 0: try: remove(self.files.pop()) except: pass if err: self.log.write('Done') return # Get logged user in Linux if sysname == 'Linux': from os import getlogin # Workaround for some Linux systems where this function does not work try: ug = getlogin() except OSError: ug = 'root' if (self.nand_mode): file = self.console_id.get() + self.suffix + '.bin' rename(self.console_id.get() + '.img', file) # Change owner of the file in Linux if sysname == 'Linux': Popen([ 'chown', '-R', ug + ':' + ug, file ]).wait() self.log.write('\nDone!\nModified NAND stored as\n' + file) return # Change owner of the out folder in Linux if sysname == 'Linux': Popen([ 'chown', '-R', ug + ':' + ug, self.sd_path ]).wait() self.log.write('Done!\nExtract your SD card and insert it into your DSi') ################################################################################################ def patcher(self, patchpath, filepath): patch_size = path.getsize(patchpath) patchfile = open(patchpath, 'rb') if patchfile.read(5) != b'PATCH': patchfile.close() raise Exception() target = open(filepath, 'r+b') # Read First Record r = patchfile.read(3) while patchfile.tell() not in [ patch_size, patch_size - 3 ]: # Unpack 3-byte pointers. offset = self.unpack_int(r) # Read size of data chunk r = patchfile.read(2) size = self.unpack_int(r) if size == 0: # RLE Record r = patchfile.read(2) rle_size = self.unpack_int(r) data = patchfile.read(1) * rle_size else: data = patchfile.read(size) # Write to file target.seek(offset) target.write(data) # Read Next Record r = patchfile.read(3) if patch_size - 3 == patchfile.tell(): trim_size = self.unpack_int(patchfile.read(3)) target.truncate(trim_size) # Cleanup target.close() patchfile.close() ################################################################################################ def unpack_int(self, bstr): # Read an n-byte big-endian integer from a byte string ( ret_val, ) = unpack_from('>I', b'\x00' * (4 - len(bstr)) + bstr) return ret_val ################################################################################################ def detect_region(self): REGION_CODES = { '484e4145': 'USA', '484e414a': 'JAP', '484e4150': 'EUR', '484e4155': 'AUS' } # Autodetect console region base = self.mounted if self.nand_mode else self.sd_path try: for app in listdir(path.join(base, 'title', '00030017')): for file in listdir(path.join(base, 'title', '00030017', app, 'content')): if file.endswith('.app'): try: self.log.write('- Detected ' + REGION_CODES[app] + ' console NAND dump') self.launcher_region = REGION_CODES[app] return app except KeyError: self.log.write('ERROR: Unsupported console region') return False self.log.write('ERROR: Could not detect console region') except OSError as e: self.log.write('ERROR: ' + e.strerror + ': ' + e.filename) return False ################################################################################################ def unlaunch_proc(self): self.log.write('\nChecking unlaunch status...') app = self.detect_region() # Stop if no supported region was found if not app: # TODO: Unmount NAND return tmd = path.join(self.mounted, 'title', '00030017', app, 'content', 'title.tmd') tmd_size = path.getsize(tmd) if tmd_size == 520: self.log.write('- Not installed. Downloading v1.4...') try: filename = urlretrieve('http://problemkaputt.de/unlau14.zip')[0] exe = path.join(sysname, '7za') proc = Popen([ exe, 'x', '-bso0', '-y', filename, 'UNLAUNCH.DSI' ]) ret_val = proc.wait() if ret_val == 0: self.files.append(filename) self.files.append('UNLAUNCH.DSI') self.log.write('- Installing unlaunch...') self.suffix = '-unlaunch' with open(tmd, 'ab') as f: with open('UNLAUNCH.DSI', 'rb') as unl: f.write(unl.read()) # Set files as read-only for file in listdir(path.join(self.mounted, 'title', '00030017', app, 'content')): file = path.join(self.mounted, 'title', '00030017', app, 'content', file) if sysname == 'Darwin': Popen([ 'chflags', 'uchg', file ]).wait() elif sysname == 'Linux': Popen([ path.join('Linux', 'fatattr'), '+R', file ]).wait() else: chmod(file, 292) else: self.log.write('ERROR: Extractor failed') # TODO: Unmount NAND except IOError: self.log.write('ERROR: Could not get unlaunch') # TODO: Unmount NAND except OSError: self.log.write('ERROR: Could not execute ' + exe) # TODO: Unmount NAND else: self.log.write('- Installed. Uninstalling...') self.suffix = '-no-unlaunch' # Set files as read-write for file in listdir(path.join(self.mounted, 'title', '00030017', app, 'content')): file = path.join(self.mounted, 'title', '00030017', app, 'content', file) if sysname == 'Darwin': Popen([ 'chflags', 'nouchg', file ]).wait() elif sysname == 'Linux': Popen([ path.join('Linux', 'fatattr'), '-R', file ]).wait() else: chmod(file, 438) with open(tmd, 'r+b') as f: f.truncate(520) Thread(target=self.unmount_nand).start() ################################################################################################ def encrypt_nand(self): self.log.write('\nEncrypting back NAND...') exe = path.join(sysname, 'twltool') try: proc = Popen([ exe, 'nandcrypt', '--in', self.console_id.get() + '.img' ]) ret_val = proc.wait() if ret_val == 0: Thread(target=self.clean).start() else: self.log.write('ERROR: Encryptor failed') except OSError: self.log.write('ERROR: Could not execute ' + exe) ################################################################################################ def remove_footer(self): self.log.write('\nRemoving No$GBA footer...') file = self.console_id.get() + '-no-footer.bin' try: copyfile(self.nand_file.get(), file) # Back-up footer info with open(self.console_id.get() + '-info.txt', 'wb') as f: f.write('eMMC CID: ' + self.cid.get() + '\r\n') f.write('Console ID: ' + self.console_id.get() + '\r\n') with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Remove footer f.truncate() # Change owner of the file in Linux if sysname == 'Linux': from os import getlogin ug = getlogin() Popen([ 'chown', '-R', ug + ':' + ug, file ]).wait() self.log.write('\nDone!\nModified NAND stored as\n' + file + '\nStored footer info in ' + self.console_id.get() + '-info.txt') except IOError: self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get())) ################################################################################################ def add_footer(self, cid, console_id): self.log.write('Adding No$GBA footer...') file = self.console_id.get() + '-footer.bin' try: copyfile(self.nand_file.get(), file) with open(file, 'r+b') as f: # Go to the No$GBA footer offset f.seek(-64, 2) # Read the footer's header :-) bstr = f.read(0x10) # Check if it already has a footer if bstr == b'DSi eMMC CID/CPU': self.log.write('ERROR: File already has a No$GBA footer') f.close() remove(file) return; # Go to the end of file f.seek(0, 2) # Write footer f.write(b'DSi eMMC CID/CPU') f.write(cid) f.write(console_id) f.write('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') self.log.write('\nDone!\nModified NAND stored as\n' + file) except IOError: self.log.write('ERROR: Could not open the file ' + path.basename(self.nand_file.get()))
class View(object): def __init__(self, master=None): self.root = master self.status_build = False self.init_view() def init_view(self): '''基本框架''' self.frm_main = LabelFrame(self.root, borderwidth=0) self.frm_main.pack(side='left', fill='y') self.frm_advance = LabelFrame(self.root, text='高级选项') # self.frm_advance.pack(expand='yes', side='right', fill='both', padx=15, pady=10) # self.frm_2 = LabelFrame(self.frm_advance, text='高级配置', width=300) # self.frm_2.pack(expand='yes', side='top', fill='both', padx=15, pady=10) self.frm_project = LabelFrame(self.frm_main, text='项目信息') self.frm_config = LabelFrame(self.frm_main, text='配置信息') self.frm_operate = LabelFrame(self.frm_main, text='操作') self.frm_status = LabelFrame(self.frm_main, text='状态') self.frm_project.pack(expand='yes', side='top', fill='both', padx=15, pady=10) self.frm_config.pack(fill='x', padx=15, pady=10) self.frm_operate.pack(fill='x', padx=15, pady=10) self.frm_status.pack(side='bottom', fill='x', padx=15, pady=10) self.init_project() self.init_config() self.init_operate() self.init_status() def init_project(self): '''项目配置''' labels = ['入口文件:', '工作目录:', '目标路径:', '图标路径:'] self.entry_value_list = list() for index, label_text in enumerate(labels): temp_strvar = StringVar() temp_label = Label(self.frm_project, text=label_text) temp_entry = Entry(self.frm_project, textvariable=temp_strvar, width=50) self.entry_value_list.append(temp_strvar) temp_label.grid(row=index % 4, column=0, padx=5, pady=5, sticky='w') temp_entry.grid(row=index % 4, column=1, padx=5, pady=5, sticky='we') self.btn_main_path = Button(self.frm_project, text='选择文件', command=self.fn_select_main) self.btn_work_path = Button(self.frm_project, text='选择路径', command=self.fn_work_path) self.btn_dist_path = Button(self.frm_project, text='选择路径', command=self.fn_dist_path) self.btn_ico_path = Button(self.frm_project, text='选择图标', command=self.fn_icon_path) self.btn_main_path.grid(row=0, column=2, padx=5, pady=5, sticky='we') self.btn_work_path.grid(row=1, column=2, padx=5, pady=5, sticky='w') self.btn_dist_path.grid(row=2, column=2, padx=5, pady=5, sticky='e') self.btn_ico_path.grid(row=3, column=2, padx=5, pady=5, sticky='e') def init_config(self): '''配置选项''' # 定义变量,并初始化 self.cfg_onefile = IntVar(value=1) self.cfg_onedir = IntVar(value=0) self.cfg_noconsole = IntVar(value=1) self.cfg_clean = IntVar(value=1) self.cfg_upx = IntVar(value=1) # UPX 默认开启 self.cfg_rename = IntVar() self.cfg_exe_name = StringVar() # 自定义配置文件 self.cfg_specfile = StringVar(value='build.spec') # 子配置框架 self.frm_config_base = LabelFrame(self.frm_config, text='基本', borderwidth=0) self.frm_config_base.pack(fill='x', padx=10, pady=5, ipady=5) self.frm_config_exe = LabelFrame(self.frm_config, text='生成执行文件类型', borderwidth=0) self.frm_config_exe.pack(fill='x', padx=10, pady=5, ipady=5) self.frm_config_other = LabelFrame(self.frm_config, text='其它', borderwidth=0) self.frm_config_other.pack(fill='x', padx=10, pady=5, ipady=5) self.frm_config_spec = LabelFrame(self.frm_config, text='配置文件', borderwidth=0) self.frm_config_spec.pack(fill='x', padx=10, pady=5, ipady=5) # 定义按钮 self.btn_noconsole = Checkbutton(self.frm_config_base, text='关闭控制台', variable=self.cfg_noconsole) self.btn_clean = Checkbutton(self.frm_config_base, text='构建前清理', variable=self.cfg_clean) self.btn_upx = Checkbutton(self.frm_config_base, text='UPX压缩', variable=self.cfg_upx) self.btn_isonefile = Checkbutton(self.frm_config_exe, text='独立执行文件', variable=self.cfg_onefile) self.btn_isonedir = Checkbutton(self.frm_config_exe, text='文件夹包含', variable=self.cfg_onedir) self.btn_rename = Checkbutton(self.frm_config_other, text='修改执行文件名', variable=self.cfg_rename) self.entry_rename = Entry(self.frm_config_other, textvariable=self.cfg_exe_name) # self.btn_rename = Checkbutton(self.frm_config_spec, text='生成配置文件', variable=self.cfg_specfile) self.entry_specfile = Entry(self.frm_config_spec, textvariable=self.cfg_specfile) # 放置按钮 self.btn_isonefile.pack(side='left', fill='x') self.btn_isonedir.pack(side='left', fill='x') self.btn_noconsole.pack(side='left', fill='x') self.btn_clean.pack(side='left', fill='x') self.btn_upx.pack(side='left', fill='x') self.btn_rename.pack(side='left', fill='x') self.entry_rename.pack(fill='x') self.entry_specfile.pack(fill='x') # 变量自动切换操作 self.cfg_onefile.trace('w', self.cfg_onefile_trace) self.cfg_onedir.trace('w', self.cfg_onedir_trace) def cfg_onefile_trace(self, *args): '''cfg_onefile 与 cfg_onedir 可以同时不选,但不能同时选中,选中独立执行文件时不能选中文件夹包''' if self.cfg_onefile.get() == 1: self.cfg_onedir.set(0) def cfg_onedir_trace(self, *args): '''cfg_onefile 与 cfg_onedir 可以同时不选,但不能同时选中,选中文件夹包含时不能选中独立执行文件''' if self.cfg_onedir.get() == 1: self.cfg_onefile.set(0) def init_operate(self): '''操作命令''' # 定义按钮 self.btn_build = Button(self.frm_operate, text='构建生成', command=self.fn_build) self.btn_clear = Button(self.frm_operate, text='清理', command=self.fn_clear) self.btn_reset = Button(self.frm_operate, text='重置', command=self.fn_reset) self.btn_advance = Button(self.frm_operate, text='高级选项', command=self.fn_toggle_advance) # 放置按钮 self.btn_build.pack(fill='x', side='left') self.btn_clear.pack(fill='x', side='left') self.btn_reset.pack(fill='x', side='left') self.btn_advance.pack(fill='x', side='right') def init_status(self): '''状态栏''' self.label_status = Label(self.frm_status, text='待命') self.label_status.grid(row=1, column=0, padx=5, pady=5, sticky='we') def fn_build(self): '''生成可执行文件''' if len(self.entry_value_list[0].get()) == 0: self.label_status['text'] = '请选择源文件' return if not self.status_build: thread_build = Thread(target=self.fn_thread) thread_build.setDaemon(True) thread_build.start() else: self.label_status['text'] = '正在打包,请稍后再操作!' def fn_thread(self): '''线程执行生成动作''' self.status_build = True self.label_status['text'] = '正在打包,请稍等。。。' try: cmd = self.fn_build_cmd() print(cmd) # pirun(cmd) system(' '.join(cmd)) # call(split(' '.join(cmd)), shell=True) self.status_build = False self.label_status['text'] = '打包成功!' except Exception as e: self.label_status['text'] = str(e) self.status_build = False def fn_clear(self): '''清理生成文件''' pass def fn_reset(self): '''重置表单内容''' for i in range(4): self.entry_value_list[i].set('') self.cfg_onefile.set(1) self.cfg_noconsole.set(1) self.cfg_clean.set(1) self.cfg_upx.set(1) self.cfg_rename.set(0) self.cfg_exe_name.set('') def fn_toggle_advance(self): '''切换高级选项界面''' if self.frm_advance.winfo_ismapped(): set_window_center(self.root, width=(self.root.winfo_width() - 400)) self.frm_advance.pack_forget() else: set_window_center(self.root, width=(self.root.winfo_width() + 400)) self.frm_advance.pack(expand='yes', side='right', fill='both', padx=15, pady=10) def fn_select_main(self): '''选择源文件''' types = (('py files', '*.py'), ('pyc files', '*.pyc'), ('spec files', '*.spec'), ('All files', '*.*')) path = filedialog.askopenfilename(filetypes=types) if not path: return _path = os.path.dirname(path) # 主文件 self.entry_value_list[0].set(path) # 工作目录 self.entry_value_list[1].set(os.path.join(_path, 'build/')) # dist目录 self.entry_value_list[2].set(os.path.join(_path, 'dist/')) def fn_work_path(self): '''选择工作目录''' path = filedialog.askdirectory() if not path: return self.entry_value_list[1].set(path) def fn_dist_path(self): '''选择生成文件目录''' path = filedialog.askdirectory() if not path: return self.entry_value_list[2].set(path) def fn_icon_path(self): '''选择图标文件''' types = (('ico files', '*.ico'), ('icns files', '*.icns'), ('All files', '*.*')) path = filedialog.askopenfilename(filetypes=types) if not path: return self.entry_value_list[3].set(path) def fn_build_cmd(self, cli=True): '''组装命令''' cmds = [] if cli is True: # 使用系统命令行 cmds.append('pyinstaller') if len(self.entry_value_list[0].get()) > 0: cmds.append(self.entry_value_list[0].get()) else: return cmds cmds.append('--windowed') cmds.append('-y') cmds.append('--noconfirm') # cmds.append('--filenames=build.spec') # cmds.append('/usr/local/bin/pyinstaller') if self.cfg_onefile.get() == 1: cmds.append('--onefile') elif self.cfg_onedir.get() == 1: cmds.append('--onedir') if self.cfg_clean.get() == 1: cmds.append('--clean') cmds.append('--noconfirm') if self.cfg_upx.get() == 0: cmds.append('--noupx') if self.cfg_noconsole.get() == 1: cmds.append('--noconsole') if len(self.entry_value_list[1].get()) > 0: cmds.append('--workpath=' + self.entry_value_list[1].get()) if len(self.entry_value_list[2].get()) > 0: cmds.append('--distpath=' + self.entry_value_list[2].get()) if len(self.entry_value_list[3].get()) > 0: cmds.append('--icon=' + self.entry_value_list[3].get()) if self.cfg_rename.get() == 1: if len(self.cfg_exe_name.get()) > 0: cmds.append('--name=' + self.cfg_exe_name.get()) # print(' '.join(cmds)) return cmds