Skip to content

Commit 049c8c9

Browse files
committed
Handle network errors gracefully instead of showing raw tracebacks
When internet connectivity is lost during downloads, the installer now catches network exceptions and shows user-friendly error messages with recovery guidance instead of crashing with a Python traceback. - Add NetworkError exception class to urlcache.py - Add retry logic to get_size() which previously had none (5 retries with backoff) - Raise NetworkError from get_block() after retries exhausted instead of re-raising raw exceptions - Wrap pre-partition download calls with interactive retry prompts (safe to retry since no disk state has changed) - Wrap post-partition do_install() calls with clean exit and repair guidance (tells user to re-run installer to trigger repair flow) - Add NetworkError handler to action_rebuild_vendorfw - Add NetworkError safety net in global exception handler Closes #409
1 parent ed29d65 commit 049c8c9

2 files changed

Lines changed: 143 additions & 27 deletions

File tree

src/main.py

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dataclasses import dataclass
55

66
import system, osenum, stub, diskutil, osinstall, asahi_firmware, m1n1, bugs
7+
from urlcache import NetworkError
78
from util import *
89

910
PART_ALIGN = psize("1MiB")
@@ -263,11 +264,25 @@ def action_install_into_container(self, avail_parts):
263264
logging.info(f"Chosen IPSW version: {ipsw.version}")
264265

265266
self.ins = stub.StubInstaller(self.sysinfo, self.dutil, self.osinfo)
266-
self.ins.load_ipsw(ipsw)
267267
self.osins = osinstall.OSInstaller(self.dutil, self.data, template)
268-
self.osins.load_package()
268+
while True:
269+
try:
270+
self.ins.load_ipsw(ipsw)
271+
self.osins.load_package()
272+
break
273+
except NetworkError as e:
274+
logging.error(f"Network error: {e}")
275+
print()
276+
p_error(f"Download failed: {e}")
277+
p_message("Please check your internet connection.")
278+
if not self.yesno("Retry"):
279+
return True
280+
print()
269281

270-
self.do_install()
282+
try:
283+
self.do_install()
284+
except NetworkError as e:
285+
self._handle_post_partition_network_error(e)
271286

272287
def action_wipe(self):
273288
p_warning("This will wipe all data on the currently selected disk.")
@@ -280,27 +295,63 @@ def action_wipe(self):
280295
template = self.choose_os()
281296

282297
self.osins = osinstall.OSInstaller(self.dutil, self.data, template)
283-
self.osins.load_package()
298+
while True:
299+
try:
300+
self.osins.load_package()
301+
break
302+
except NetworkError as e:
303+
logging.error(f"Network error: {e}")
304+
print()
305+
p_error(f"Download failed: {e}")
306+
p_message("Please check your internet connection.")
307+
if not self.yesno("Retry"):
308+
return True
309+
print()
284310

285311
min_size = STUB_SIZE + (self.osins.min_size if self.expert else self.osins.min_recommended_size)
286312
print()
287313
p_message(f"Minimum required space for this OS: {ssize(min_size)}")
288314

289315
start, end = self.dutil.get_disk_usable_range(self.cur_disk)
290-
os_size = self.get_os_size_and_info(end - start, min_size, template)
316+
while True:
317+
try:
318+
os_size = self.get_os_size_and_info(end - start, min_size, template)
319+
break
320+
except NetworkError as e:
321+
logging.error(f"Network error: {e}")
322+
print()
323+
p_error(f"Download failed: {e}")
324+
p_message("Please check your internet connection.")
325+
if not self.yesno("Retry"):
326+
return True
327+
print()
291328

292329
p_progress(f"Partitioning the whole disk ({self.cur_disk})")
293330
self.part = self.dutil.partitionDisk(self.cur_disk, "apfs", self.osins.name, STUB_SIZE)
294331

295332
p_progress(f"Creating new stub macOS named {self.osins.name}")
296333
logging.info(f"Creating stub macOS: {self.osins.name}")
297-
self.do_install(os_size)
334+
try:
335+
self.do_install(os_size)
336+
except NetworkError as e:
337+
self._handle_post_partition_network_error(e)
298338

299339
def action_install_into_free(self, avail_free):
300340
template = self.choose_os()
301341

302342
self.osins = osinstall.OSInstaller(self.dutil, self.data, template)
303-
self.osins.load_package()
343+
while True:
344+
try:
345+
self.osins.load_package()
346+
break
347+
except NetworkError as e:
348+
logging.error(f"Network error: {e}")
349+
print()
350+
p_error(f"Download failed: {e}")
351+
p_message("Please check your internet connection.")
352+
if not self.yesno("Retry"):
353+
return True
354+
print()
304355

305356
min_size = STUB_SIZE + (self.osins.min_size if self.expert else self.osins.min_recommended_size)
306357
print()
@@ -327,13 +378,27 @@ def action_install_into_free(self, avail_free):
327378
print()
328379
p_message(f"Available free space: {ssize(free_part.size)}")
329380

330-
os_size = self.get_os_size_and_info(free_part.size, min_size, template)
381+
while True:
382+
try:
383+
os_size = self.get_os_size_and_info(free_part.size, min_size, template)
384+
break
385+
except NetworkError as e:
386+
logging.error(f"Network error: {e}")
387+
print()
388+
p_error(f"Download failed: {e}")
389+
p_message("Please check your internet connection.")
390+
if not self.yesno("Retry"):
391+
return True
392+
print()
331393

332394
p_progress(f"Creating new stub macOS named {self.osins.name}")
333395
logging.info(f"Creating stub macOS: {self.osins.name}")
334396
self.part = self.dutil.addPartition(free_part.name, "apfs", self.osins.name, STUB_SIZE)
335397

336-
self.do_install(os_size)
398+
try:
399+
self.do_install(os_size)
400+
except NetworkError as e:
401+
self._handle_post_partition_network_error(e)
337402

338403
def get_os_size_and_info(self, free_size, min_size, template):
339404
os_size = None
@@ -488,9 +553,19 @@ def action_rebuild_vendorfw(self, oses):
488553
p_message("Unable to rebuild firmware")
489554
return False
490555

491-
self.ins.load_ipsw(ipsw)
492-
self.ins.load_identity()
493-
self.ins.collect_firmware(fw_pkg)
556+
try:
557+
self.ins.load_ipsw(ipsw)
558+
self.ins.load_identity()
559+
self.ins.collect_firmware(fw_pkg)
560+
except NetworkError as e:
561+
logging.error(f"Network error during firmware rebuild: {e}")
562+
print()
563+
p_error(f"Firmware rebuild failed: {e}")
564+
p_message("Please check your internet connection and try again.")
565+
print()
566+
p_message("Press enter to return to the main menu.")
567+
self.input()
568+
return True
494569
fw_pkg.close()
495570

496571
p_plain(f" Copying firmware into {target.name} partition...")
@@ -505,6 +580,23 @@ def action_rebuild_vendorfw(self, oses):
505580

506581
return True
507582

583+
def _handle_post_partition_network_error(self, e):
584+
logging.error(f"Network error during installation: {e}")
585+
print()
586+
p_error("Installation failed due to a network error.")
587+
p_error(f" {e}")
588+
print()
589+
p_message("Please check your internet connection, then re-run the installer.")
590+
p_message("The installer will detect the incomplete installation and offer")
591+
p_message("to repair it.")
592+
print()
593+
p_warning("If you need to file a bug report, please attach the log file:")
594+
p_warning(f" {os.getcwd()}/installer.log")
595+
print()
596+
p_message("Press enter to exit.")
597+
self.input()
598+
sys.exit(1)
599+
508600
def do_install(self, total_size=None):
509601
p_progress(f"Installing stub macOS into {self.part.name} ({self.part.label})")
510602

@@ -1151,6 +1243,13 @@ def main_loop(self):
11511243
print()
11521244
logging.info("KeyboardInterrupt")
11531245
p_error("Interrupted")
1246+
except NetworkError as e:
1247+
logging.exception("Network error")
1248+
p_error(f"Installation failed due to a network error: {e}")
1249+
print()
1250+
p_message("Please check your internet connection and try again.")
1251+
p_warning("If you need to file a bug report, please attach the log file:")
1252+
p_warning(f" {os.getcwd()}/installer.log")
11541253
except subprocess.CalledProcessError as e:
11551254
cmd = shlex.join(e.cmd)
11561255
p_error(f"Failed to run process: {cmd}")

src/urlcache.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
from dataclasses import dataclass
44

55
from urllib import parse
6-
from http.client import HTTPSConnection, HTTPConnection
6+
from http.client import HTTPSConnection, HTTPConnection, HTTPException
77
from util import *
88

9+
class NetworkError(Exception):
10+
pass
11+
912
@dataclass
1013
class CacheBlock:
1114
idx: int
@@ -78,19 +81,31 @@ def seekable(self):
7881
return True
7982

8083
def get_size(self):
81-
for i in range(10):
82-
con = self.get_con()
83-
con.request("HEAD", self.url.path, headers={"Connection":" keep-alive"})
84-
res = con.getresponse()
85-
res.read()
86-
loc = res.getheader("Location", None)
87-
if loc is not None:
88-
self.url = parse.urlparse(loc)
89-
self.con = None
90-
continue
91-
return int(res.getheader("Content-length"))
92-
93-
raise Exception("Maximum number of redirects reached")
84+
retries = 5
85+
sleep = 1
86+
for retry in range(retries + 1):
87+
try:
88+
for i in range(10):
89+
con = self.get_con()
90+
con.request("HEAD", self.url.path, headers={"Connection":" keep-alive"})
91+
res = con.getresponse()
92+
res.read()
93+
loc = res.getheader("Location", None)
94+
if loc is not None:
95+
self.url = parse.urlparse(loc)
96+
self.con = None
97+
continue
98+
return int(res.getheader("Content-length"))
99+
raise Exception("Maximum number of redirects reached")
100+
except (OSError, HTTPException) as e:
101+
if retry == retries:
102+
raise NetworkError(
103+
f"Failed to connect to {self.url.netloc} after multiple retries"
104+
) from e
105+
p_warning(f"Connection error ({e}), retrying... ({retry + 1}/{retries})")
106+
time.sleep(sleep)
107+
self.close_connection()
108+
sleep += 1
94109

95110
def get_partial(self, off, size, bypass_cache=False):
96111
path = self.url.path
@@ -147,7 +162,9 @@ def get_block(self, blk, readahead=1):
147162
except Exception as e:
148163
if retry == retries:
149164
p_error(f"Exceeded maximum retries downloading data.")
150-
raise
165+
raise NetworkError(
166+
f"Download failed: lost connection to {self.url.netloc}"
167+
) from e
151168
p_warning(f"Error downloading data ({e}), retrying... ({retry + 1}/{retries})")
152169
time.sleep(sleep)
153170
self.close_connection()

0 commit comments

Comments
 (0)