From c44a15d4c33b52f96a2fc8f2df43e52ba0de6eb2 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Mon, 16 Dec 2019 11:40:29 +1300 Subject: [PATCH] straigthen out refresh logic and handle failed update better --- subiquity/controllers/refresh.py | 52 ++++++------- subiquity/ui/views/refresh.py | 122 ++++++++++++++++++------------- 2 files changed, 96 insertions(+), 78 deletions(-) diff --git a/subiquity/controllers/refresh.py b/subiquity/controllers/refresh.py index 26699f7a..7b539d77 100644 --- a/subiquity/controllers/refresh.py +++ b/subiquity/controllers/refresh.py @@ -33,16 +33,10 @@ log = logging.getLogger('subiquity.controllers.refresh') class CheckState(enum.IntEnum): - NOT_STARTED = enum.auto() - CHECKING = enum.auto() - FAILED = enum.auto() - + UNKNOWN = enum.auto() AVAILABLE = enum.auto() UNAVAILABLE = enum.auto() - def is_definite(self): - return self in [self.AVAILABLE, self.UNAVAILABLE] - class RefreshController(BaseController): @@ -53,7 +47,6 @@ class RefreshController(BaseController): def __init__(self, app): super().__init__(app) self.snap_name = os.environ.get("SNAP_NAME", "subiquity") - self.check_state = CheckState.NOT_STARTED self.configure_task = None self.check_task = None @@ -64,8 +57,20 @@ class RefreshController(BaseController): def start(self): self.configure_task = schedule_task(self.configure_snapd()) - self.check_task = SingleInstanceTask(self.check_for_update) - self.check_task.start_sync() + self.check_task_starter = SingleInstanceTask( + self.check_for_update, propagate_errors=False) + self.check_task_starter.start_sync() + + @property + def check_state(self): + task = self.check_task_starter.task + if task is None: + return CheckState.UNKNOWN + if not task.done(): + return CheckState.UNKNOWN + if task.exception(): + return CheckState.UNAVAILABLE + return task.result() async def configure_snapd(self): try: @@ -124,35 +129,24 @@ class RefreshController(BaseController): return 'stable/ubuntu-' + release def snapd_network_changed(self): - if not self.check_state.is_definite(): - self.check_task.start_sync() + if self.check_state == CheckState.UNKNOWN: + self.check_task_starter.start_sync() async def check_for_update(self): await self.configure_task # If we restarted into this version, don't check for a new version. if self.app.updated: - self.check_state = CheckState.UNAVAILABLE - return - self.check_state = CheckState.CHECKING - try: - result = await self.app.snapd.get('v2/find', select='refresh') - except requests.exceptions.RequestException: - log.exception("checking for update") - self.check_state = CheckState.FAILED - return + return CheckState.UNAVAILABLE + result = await self.app.snapd.get('v2/find', select='refresh') log.debug("check_for_update received %s", result) for snap in result["result"]: if snap["name"] == self.snap_name: - self.check_state = CheckState.AVAILABLE self.new_snap_version = snap["version"] log.debug( "new version of snap available: %r", self.new_snap_version) - break - else: - self.check_state = CheckState.UNAVAILABLE - if self.showing: - self.ui.body.update_check_state() + return CheckState.AVAILABLE + return CheckState.UNAVAILABLE async def start_update(self): update_marker = os.path.join(self.app.state_dir, 'updating') @@ -178,8 +172,8 @@ class RefreshController(BaseController): self.offered_first_time = True elif index == 2: if not self.offered_first_time: - if self.check_state in [CheckState.AVAILABLE, - CheckState.CHECKING]: + if self.check_state in [CheckState.UNKNOWN, + CheckState.AVAILABLE]: show = True else: raise AssertionError("unexpected index {}".format(index)) diff --git a/subiquity/ui/views/refresh.py b/subiquity/ui/views/refresh.py index a2d9dd53..c59503c4 100644 --- a/subiquity/ui/views/refresh.py +++ b/subiquity/ui/views/refresh.py @@ -94,6 +94,18 @@ class TaskProgress(WidgetWrap): self.spinner.spin() +def exc_message(exc): + try: + result = exc.response.json() + except (AttributeError, json.decoder.JSONDecodeError): + message = None + else: + message = result.get("result", {}).get("message") + if message is not None: + return message + return"Unknown error: {}".format(exc) + + class RefreshView(BaseView): checking_title = _("Checking for installer update...") @@ -102,8 +114,8 @@ class RefreshView(BaseView): "installer is available." ) - failed_title = _("Contacting the snap store failed") - failed_excerpt = _( + check_failed_title = _("Contacting the snap store failed") + check_failed_excerpt = _( "Contacting the snap store failed:" ) @@ -119,33 +131,22 @@ class RefreshView(BaseView): "installer will restart automatically when the download is complete." ) + update_failed_title = _("Update failed") + update_failed_excerpt = _( + "Downloading and applying the update:" + ) + def __init__(self, controller): self.controller = controller self.spinner = Spinner(self.controller.loop, style="dots") - if self.controller.check_state == CheckState.CHECKING: + if self.controller.check_state == CheckState.UNKNOWN: self.check_state_checking() - elif self.controller.check_state == CheckState.AVAILABLE: - self.check_state_available() else: - raise AssertionError( - "instantiating the view with check_state {}".format( - self.controller.check_state)) + self.check_state_available() super().__init__(self._w) - def update_check_state(self): - if self.controller.check_state == CheckState.UNAVAILABLE: - self.done() - elif self.controller.check_state == CheckState.FAILED: - self.check_state_failed() - elif self.controller.check_state == CheckState.AVAILABLE: - self.check_state_available() - else: - raise AssertionError( - "update_check_state with check_state {}".format( - self.controller.check_state)) - def check_state_checking(self): self.spinner.start() @@ -159,6 +160,36 @@ class RefreshView(BaseView): self.title = self.checking_title self.controller.ui.set_header(self.title) self._w = screen(rows, buttons, excerpt=_(self.checking_excerpt)) + schedule_task(self._wait_check_result()) + + async def _wait_check_result(self): + try: + check_state = await self.controller.check_task.task + except Exception as e: + self.check_state_failed(e) + if check_state == CheckState.AVAILABLE: + self.check_state_available() + else: + self.done() + + def check_state_failed(self, exc): + self.spinner.stop() + + rows = [Text(exc_message(exc))] + + buttons = button_pile([ + done_btn(_("Try again"), on_press=self.try_check_again), + done_btn(_("Continue without updating"), on_press=self.done), + other_btn(_("Back"), on_press=self.cancel), + ]) + buttons.base_widget.focus_position = 1 + + self.title = self.failed_title + self._w = screen(rows, buttons, excerpt=_(self.failed_excerpt)) + + def try_check_again(self, sender=None): + self.controller.snapd_network_changed() + self.check_state_checking() def check_state_available(self, sender=None): self.spinner.stop() @@ -191,34 +222,6 @@ class RefreshView(BaseView): self.controller.ui.set_header(self.available_title) self._w = screen(rows, buttons, excerpt=excerpt) - def check_state_failed(self, exc): - self.spinner.stop() - - try: - result = exc.response.json() - except (AttributeError, json.decoder.JSONDecodeError): - message = None - else: - message = result.get("result", {}).get("message") - if message is None: - message = "Unknown error: {}".format(exc) - - rows = [Text(message)] - - buttons = button_pile([ - done_btn(_("Try again"), on_press=self.try_again), - done_btn(_("Continue without updating"), on_press=self.done), - other_btn(_("Back"), on_press=self.cancel), - ]) - buttons.base_widget.focus_position = 1 - - self.title = self.failed_title - self._w = screen(rows, buttons, excerpt=_(self.failed_excerpt)) - - def try_again(self, sender=None): - self.controller.snapd_network_changed() - self.check_state_checking() - def update(self, sender=None): self.spinner.stop() @@ -238,7 +241,7 @@ class RefreshView(BaseView): try: change_id = await self.controller.start_update() except requests.exceptions.RequestException as e: - self.check_state_failed(e) + self.update_failed(exc_message(e)) return while True: change = await self.controller.get_progress(change_id) @@ -247,9 +250,30 @@ class RefreshView(BaseView): # getting restarted by snapd... self.done() return + if change['status'] not in ['Do', 'Doing']: + self.update_failed(change.get('err', "Unknown error")) + return self.update_progress(change) await asyncio.sleep(0.1) + def try_update_again(self, sender=None): + self.check_state_available() + + def update_failed(self, msg): + self.spinner.stop() + + rows = [Text(msg)] + + buttons = button_pile([ + done_btn(_("Try again"), on_press=self.try_update_again), + done_btn(_("Continue without updating"), on_press=self.done), + other_btn(_("Back"), on_press=self.cancel), + ]) + buttons.base_widget.focus_position = 1 + + self.title = self.update_failed_title + self._w = screen(rows, buttons, excerpt=_(self.update_failed_excerpt)) + def update_progress(self, change): for task in change['tasks']: tid = task['id']