straigthen out refresh logic and handle failed update better

This commit is contained in:
Michael Hudson-Doyle 2019-12-16 11:40:29 +13:00
parent 930d479e85
commit c44a15d4c3
2 changed files with 96 additions and 78 deletions

View File

@ -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:
return CheckState.UNAVAILABLE
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
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))

View File

@ -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']