예제 #1
0
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()
예제 #2
0
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()))
예제 #3
0
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()
예제 #4
0
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()
예제 #5
0
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()))
예제 #6
0
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