custominstall: support new events, check if save3ds_fuse runs successfully, fix seed loading, don't continue if cia fails to load (temp maybe)

This commit is contained in:
Ian Burgwin
2020-07-19 20:30:34 -07:00
parent ed7fc99ff1
commit 187e27fc95

View File

@@ -9,8 +9,10 @@ from os import makedirs, scandir
from os.path import dirname, join from os.path import dirname, join
from random import randint from random import randint
from hashlib import sha256 from hashlib import sha256
from sys import platform from locale import getpreferredencoding
from sys import exc_info, platform
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from traceback import format_exception
from typing import BinaryIO, TYPE_CHECKING from typing import BinaryIO, TYPE_CHECKING
import subprocess import subprocess
@@ -20,7 +22,7 @@ if TYPE_CHECKING:
from events import Events from events import Events
from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb from pyctr.crypto import CryptoEngine, Keyslot, load_seeddb, get_seed
from pyctr.type.cia import CIAReader, CIASection from pyctr.type.cia import CIAReader, CIASection
from pyctr.type.ncch import NCCHSection from pyctr.type.ncch import NCCHSection
from pyctr.util import roundup from pyctr.util import roundup
@@ -147,7 +149,7 @@ class CustomInstall:
self.cifinish_out = cifinish_out self.cifinish_out = cifinish_out
self.movable = movable self.movable = movable
def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str): def copy_with_progress(self, src: BinaryIO, dst: BinaryIO, size: int, path: str, fire_event: bool = True):
left = size left = size
cipher = self.crypto.create_ctr_cipher(Keyslot.SD, self.crypto.sd_path_to_iv(path)) cipher = self.crypto.create_ctr_cipher(Keyslot.SD, self.crypto.sd_path_to_iv(path))
while left > 0: while left > 0:
@@ -156,9 +158,10 @@ class CustomInstall:
dst.write(data) dst.write(data)
left -= to_read left -= to_read
total_read = size - left total_read = size - left
self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576) if fire_event:
self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576)
def start(self): def start(self, continue_on_fail=True):
crypto = self.crypto crypto = self.crypto
# TODO: Move a lot of these into their own methods # TODO: Move a lot of these into their own methods
self.log("Finding path to install to...") self.log("Finding path to install to...")
@@ -181,14 +184,19 @@ class CustomInstall:
# Now loop through all provided cia files # Now loop through all provided cia files
for c in self.cias: for idx, c in enumerate(self.cias):
self.log('Reading ' + c) self.log('Reading ' + c)
try: try:
cia = CIAReader(c) cia = CIAReader(c)
except Exception as e: except Exception as e:
self.log(f'Failed to load file: {type(e).__name__}: {e}') self.event.on_error(exc_info())
continue if continue_on_fail:
continue
else:
return
self.event.on_cia_start(idx)
self.cia = cia self.cia = cia
@@ -256,7 +264,8 @@ class CustomInstall:
self.log(f'Writing {enc_path}...') self.log(f'Writing {enc_path}...')
with cia.open_raw_section(CIASection.TitleMetadata) as s: with cia.open_raw_section(CIASection.TitleMetadata) as s:
with open(join(content_root, tmd_filename), 'wb') as o: with open(join(content_root, tmd_filename), 'wb') as o:
self.copy_with_progress(s, o, cia.sections[CIASection.TitleMetadata].size, enc_path) self.copy_with_progress(s, o, cia.sections[CIASection.TitleMetadata].size, enc_path,
fire_event=False)
# write each content # write each content
for co in cia.content_info: for co in cia.content_info:
@@ -375,7 +384,7 @@ class CustomInstall:
title_info_entries[cia.tmd.title_id] = b''.join(title_info_entry_data) title_info_entries[cia.tmd.title_id] = b''.join(title_info_entry_data)
cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (cia.contents[0].seed if cia.contents[0].flags.uses_seed else None)} cifinish_data[int(cia.tmd.title_id, 16)] = {'seed': (get_seed(cia.contents[0].program_id) if cia.contents[0].flags.uses_seed else None)}
# This is saved regardless if any titles were installed, so the file can be upgraded just in case. # This is saved regardless if any titles were installed, so the file can be upgraded just in case.
save_cifinish(cifinish_path, cifinish_data) save_cifinish(cifinish_path, cifinish_data)
@@ -394,7 +403,14 @@ class CustomInstall:
# extract the title database to add our own entry to # extract the title database to add our own entry to
self.log('Extracting Title Database...') self.log('Extracting Title Database...')
subprocess.run(save3ds_fuse_common_args + ['-x']) out = subprocess.run(save3ds_fuse_common_args + ['-x'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding=getpreferredencoding())
if out.returncode:
for l in out.stdout.split('\n'):
self.log(l)
return False
for title_id, entry in title_info_entries.items(): for title_id, entry in title_info_entries.items():
# write the title info entry to the temp directory # write the title info entry to the temp directory
@@ -403,13 +419,23 @@ class CustomInstall:
# import the directory, now including our title # import the directory, now including our title
self.log('Importing into Title Database...') self.log('Importing into Title Database...')
subprocess.run(save3ds_fuse_common_args + ['-i']) out = subprocess.run(save3ds_fuse_common_args + ['-i'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding=getpreferredencoding())
if out.returncode:
for l in out.stdout.split('\n'):
self.log(l)
return False
self.log('FINAL STEP:\nRun custom-install-finalize through homebrew launcher.') self.log('FINAL STEP:')
self.log('Run custom-install-finalize through homebrew launcher.')
self.log('This will install a ticket and seed if required.') self.log('This will install a ticket and seed if required.')
return True
else: else:
self.log('Did not install any titles.', 2) self.log('Did not install any titles.', 2)
return None
def get_sd_path(self): def get_sd_path(self):
sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex()) sd_path = join(self.sd, 'Nintendo 3DS', self.crypto.id0.hex())
@@ -479,7 +505,16 @@ if __name__ == "__main__":
def percent_handle(total_percent, total_read, size): def percent_handle(total_percent, total_read, size):
installer.log(f' {total_percent:>5.1f}% {total_read:>.1f} MiB / {size:.1f} MiB\r', end='') installer.log(f' {total_percent:>5.1f}% {total_read:>.1f} MiB / {size:.1f} MiB\r', end='')
def error(exc):
for line in format_exception(*exc):
for line2 in line.split('\n')[:-1]:
installer.log(line2)
installer.event.on_log_msg += log_handle installer.event.on_log_msg += log_handle
installer.event.update_percentage += percent_handle installer.event.update_percentage += percent_handle
installer.event.on_error += error
installer.start() result = installer.start(continue_on_fail=False)
if result is False:
# save3ds_fuse failed
installer.log('NOTE: Once save3ds_fuse is fixed, run the same command again with --skip-contents')