ci-gui: replace listview with treeview, load titles before adding to list and show reasons for failure, verify cifinish.bin after choosing SD card

This commit is contained in:
Ian Burgwin
2021-02-08 23:45:26 -08:00
parent 0dcaaedda7
commit 8ed6ca54cc

220
ci-gui.py
View File

@@ -5,7 +5,7 @@
# You can find the full license text in LICENSE.md in the root of this project.
from os import environ, scandir
from os.path import abspath, join, isfile, dirname
from os.path import abspath, basename, dirname, join, isfile
from sys import exc_info, platform
from threading import Thread, Lock
from time import strftime
@@ -18,8 +18,11 @@ from typing import TYPE_CHECKING
from pyctr.crypto.engine import b9_paths
from pyctr.util import config_dirs
from pyctr.type.cdn import CDNError
from pyctr.type.cia import CIAError
from pyctr.type.tmd import TitleMetadataError
from custominstall import CustomInstall, CI_VERSION
from custominstall import CustomInstall, CI_VERSION, load_cifinish, InvalidCIFinishError
if TYPE_CHECKING:
from typing import Dict, List
@@ -98,6 +101,72 @@ class ConsoleFrame(ttk.Frame):
self.text.configure(state=tk.DISABLED)
def simple_listbox_frame(parent, title: 'str', items: 'List[str]'):
frame = ttk.LabelFrame(parent, text=title)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL)
scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
box = tk.Listbox(frame, highlightthickness=0, yscrollcommand=scrollbar.set, selectmode=tk.EXTENDED)
box.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar.config(command=box.yview)
box.insert(tk.END, *items)
box.config(height=clamp(len(items), 3, 10))
return frame
class TitleReadFailResults(tk.Toplevel):
def __init__(self, parent: tk.Tk = None, *, failed: 'Dict[str, str]'):
super().__init__(parent)
self.parent = parent
self.wm_withdraw()
self.wm_transient(self.parent)
self.grab_set()
self.wm_title('Failed to add titles')
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
outer_container = ttk.Frame(self)
outer_container.grid(sticky=tk.NSEW)
outer_container.rowconfigure(0, weight=0)
outer_container.rowconfigure(1, weight=1)
outer_container.columnconfigure(0, weight=1)
message_label = ttk.Label(outer_container, text="Some titles couldn't be added.")
message_label.grid(row=0, column=0, sticky=tk.NSEW, padx=10, pady=10)
treeview_frame = ttk.Frame(self)
treeview_frame.grid(row=1, column=0, sticky=tk.NSEW)
treeview_frame.rowconfigure(0, weight=1)
treeview_frame.columnconfigure(0, weight=1)
treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL)
treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
treeview = ttk.Treeview(treeview_frame, yscrollcommand=treeview_scrollbar.set)
treeview.grid(row=0, column=0, sticky=tk.NSEW)
treeview.configure(columns=('filepath', 'reason'), show='headings')
treeview.column('filepath', width=200, anchor=tk.W)
treeview.heading('filepath', text='File path')
treeview.column('reason', width=400, anchor=tk.W)
treeview.heading('reason', text='Reason')
treeview_scrollbar.configure(command=treeview.yview)
for path, reason in failed.items():
treeview.insert('', tk.END, text=path, iid=path, values=(basename(path), reason))
self.wm_deiconify()
class InstallResults(tk.Toplevel):
def __init__(self, parent: tk.Tk = None, *, install_state: 'Dict[str, List[str]]', copied_3dsx: bool):
super().__init__(parent)
@@ -137,34 +206,16 @@ class InstallResults(tk.Toplevel):
if install_state['installed']:
outer_container.rowconfigure(1, weight=1)
frame = self.simple_listbox_frame(outer_container, 'Installed', install_state['installed'])
frame = simple_listbox_frame(outer_container, 'Installed', install_state['installed'])
frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
if install_state['failed']:
outer_container.rowconfigure(2, weight=1)
frame = self.simple_listbox_frame(outer_container, 'Failed', install_state['failed'])
frame = simple_listbox_frame(outer_container, 'Failed', install_state['failed'])
frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10, pady=(0, 10))
self.wm_deiconify()
def simple_listbox_frame(self, parent, title: 'str', items: 'List[str]'):
frame = ttk.LabelFrame(parent, text=title)
frame.rowconfigure(0, weight=1)
frame.columnconfigure(0, weight=1)
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL)
scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
box = tk.Listbox(frame, highlightthickness=0, yscrollcommand=scrollbar.set, selectmode=tk.EXTENDED)
box.grid(row=0, column=0, sticky=tk.NSEW)
scrollbar.config(command=box.yview)
box.insert(tk.END, *items)
box.config(height=clamp(len(items), 3, 10))
return frame
class CustomInstallGUI(ttk.Frame):
console = None
@@ -173,6 +224,9 @@ class CustomInstallGUI(ttk.Frame):
super().__init__(parent)
self.parent = parent
# readers to give to CustomInstall at the install
self.readers = {}
self.lock = Lock()
self.log_messages = []
@@ -202,6 +256,16 @@ class CustomInstallGUI(ttk.Frame):
f = fd.askdirectory(parent=parent, title='Select SD root (the directory or drive that contains '
'"Nintendo 3DS")', initialdir=file_parent, mustexist=True)
if f:
cifinish_path = join(f, 'cifinish.bin')
try:
load_cifinish(cifinish_path)
except InvalidCIFinishError:
self.show_error(f'{cifinish_path} was corrupt!\n\n'
f'This could mean an issue with the SD card or the filesystem. Please check it for errors.\n'
f'It is also possible, though less likely, to be an issue with custom-install.\n\n'
f'Stopping now to prevent possible issues. If you want to try again, delete cifinish.bin from the SD card and re-run custom-install.')
return
sd_selected.delete('1.0', tk.END)
sd_selected.insert(tk.END, f)
@@ -251,19 +315,26 @@ class CustomInstallGUI(ttk.Frame):
# ---------------------------------------------------------------- #
# create buttons to add cias
listbox_buttons = ttk.Frame(self)
listbox_buttons.grid(row=1, column=0)
titlelist_buttons = ttk.Frame(self)
titlelist_buttons.grid(row=1, column=0)
def add_cias_callback():
files = fd.askopenfilenames(parent=parent, title='Select CIA files', filetypes=[('CIA files', '*.cia')],
initialdir=file_parent)
results = {}
for f in files:
self.add_cia(f)
success, reason = self.add_cia(f)
if not success:
results[f] = reason
add_cias = ttk.Button(listbox_buttons, text='Add CIAs', command=add_cias_callback)
if results:
title_read_fail_window = TitleReadFailResults(self.parent, failed=results)
title_read_fail_window.focus()
add_cias = ttk.Button(titlelist_buttons, text='Add CIAs', command=add_cias_callback)
add_cias.grid(row=0, column=0)
def add_cias_callback():
def add_cdn_callback():
d = fd.askdirectory(parent=parent, title='Select folder containing title contents in CDN format',
initialdir=file_parent)
if d:
@@ -272,44 +343,55 @@ class CustomInstallGUI(ttk.Frame):
else:
self.show_error('tmd file not found in the CDN directory:\n' + d)
add_cias = ttk.Button(listbox_buttons, text='Add CDN title folder', command=add_cias_callback)
add_cias.grid(row=0, column=1)
add_cdn = ttk.Button(titlelist_buttons, text='Add CDN title folder', command=add_cdn_callback)
add_cdn.grid(row=0, column=1)
def add_dirs_callback():
d = fd.askdirectory(parent=parent, title='Select folder containing CIA files', initialdir=file_parent)
if d:
results = {}
for f in scandir(d):
if f.name.lower().endswith('.cia'):
self.add_cia(f.path)
success, reason = self.add_cia(f.path)
if not success:
results[f] = reason
add_dirs = ttk.Button(listbox_buttons, text='Add folder', command=add_dirs_callback)
if results:
title_read_fail_window = TitleReadFailResults(self.parent, failed=results)
title_read_fail_window.focus()
add_dirs = ttk.Button(titlelist_buttons, text='Add folder', command=add_dirs_callback)
add_dirs.grid(row=0, column=2)
def remove_selected_callback():
indexes = self.cia_listbox.curselection()
n = 0
for i in indexes:
self.cia_listbox.delete(i - n)
n += 1
for entry in self.treeview.selection():
self.remove_cia(entry)
remove_selected = ttk.Button(listbox_buttons, text='Remove selected', command=remove_selected_callback)
remove_selected = ttk.Button(titlelist_buttons, text='Remove selected', command=remove_selected_callback)
remove_selected.grid(row=0, column=3)
# ---------------------------------------------------------------- #
# create listbox
listbox_frame = ttk.Frame(self)
listbox_frame.grid(row=2, column=0, sticky=tk.NSEW)
listbox_frame.rowconfigure(0, weight=1)
listbox_frame.columnconfigure(0, weight=1)
# create treeview
treeview_frame = ttk.Frame(self)
treeview_frame.grid(row=2, column=0, sticky=tk.NSEW)
treeview_frame.rowconfigure(0, weight=1)
treeview_frame.columnconfigure(0, weight=1)
cia_listbox_scrollbar = ttk.Scrollbar(listbox_frame, orient=tk.VERTICAL)
cia_listbox_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
treeview_scrollbar = ttk.Scrollbar(treeview_frame, orient=tk.VERTICAL)
treeview_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
self.cia_listbox = tk.Listbox(listbox_frame, highlightthickness=0, yscrollcommand=cia_listbox_scrollbar.set,
selectmode=tk.EXTENDED)
self.cia_listbox.grid(row=0, column=0, sticky=tk.NSEW)
self.treeview = ttk.Treeview(treeview_frame, yscrollcommand=treeview_scrollbar.set)
self.treeview.grid(row=0, column=0, sticky=tk.NSEW)
self.treeview.configure(columns=('filepath', 'titleid', 'titlename'), show='headings')
cia_listbox_scrollbar.config(command=self.cia_listbox.yview)
self.treeview.column('filepath', width=200, anchor=tk.W)
self.treeview.heading('filepath', text='File path')
self.treeview.column('titleid', width=50, anchor=tk.W)
self.treeview.heading('titleid', text='Title ID')
self.treeview.column('titlename', width=150, anchor=tk.W)
self.treeview.heading('titlename', text='Title name')
treeview_scrollbar.configure(command=self.treeview.yview)
# ---------------------------------------------------------------- #
# create progressbar
@@ -354,7 +436,26 @@ class CustomInstallGUI(ttk.Frame):
def add_cia(self, path):
path = abspath(path)
self.cia_listbox.insert(tk.END, path)
try:
reader = CustomInstall.get_reader(path)
except (CIAError, CDNError, TitleMetadataError):
return False, 'Failed to read as a CIA or CDN title, probably corrupt'
except Exception as e:
return False, f'Exception occurred: {type(e).__name__}: {e}'
if reader.tmd.title_id.startswith('00048'):
return False, 'DSiWare is not supported'
try:
title_name = reader.contents[0].exefs.icon.get_app_title().short_desc
except:
title_name = '(No title)'
self.treeview.insert('', tk.END, text=path, iid=path, values=(path, reader.tmd.title_id, title_name))
self.readers[path] = reader
return True, ''
def remove_cia(self, path):
self.treeview.delete(path)
del self.readers[path]
def open_console(self):
if self.console:
@@ -428,13 +529,13 @@ class CustomInstallGUI(ttk.Frame):
return
self.disable_buttons()
self.log('Starting install...')
cias = self.cia_listbox.get(0, tk.END)
if not len(cias):
if not len(self.readers):
self.show_error('There are no titles added to install.')
return
self.log('Starting install...')
if taskbar:
taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL)
@@ -445,8 +546,10 @@ class CustomInstallGUI(ttk.Frame):
skip_contents=self.skip_contents_var.get() == 1,
overwrite_saves=self.overwrite_saves_var.get() == 1)
installer.readers = self.readers.values()
finished_percent = 0
max_percentage = 100 * len(cias)
max_percentage = 100 * len(self.readers)
self.progressbar.config(maximum=max_percentage)
def ci_on_log_msg(message, *args, **kwargs):
@@ -478,17 +581,6 @@ class CustomInstallGUI(ttk.Frame):
installer.event.on_error += ci_on_error
installer.event.on_cia_start += ci_on_cia_start
try:
installer.prepare_titles(cias)
except Exception as e:
for line in format_exception(*exc_info()):
for line2 in line.split('\n')[:-1]:
installer.log(line2)
self.show_error('An error occurred when trying to read the files.')
self.open_console()
self.enable_buttons()
return
if self.skip_contents_var.get() != 1:
total_size, free_space = installer.check_size()
if total_size > free_space: