From b91c9611650a9237c911238b5b01e4ea05d133fe Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 7 Mar 2019 22:20:29 +1300 Subject: [PATCH] implement screens up to the point of offering the update don't display anything about check failures yet, or actually allow the user to start the update --- subiquity/controllers/refresh.py | 92 ++++++++++++++++++- subiquity/snapd.py | 1 + subiquity/ui/views/refresh.py | 152 +++++++++++++++++++++++++++++++ subiquitycore/controller.py | 2 + subiquitycore/core.py | 1 + subiquitycore/signals.py | 4 +- 6 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 subiquity/ui/views/refresh.py diff --git a/subiquity/controllers/refresh.py b/subiquity/controllers/refresh.py index e8229aef..65af60ad 100644 --- a/subiquity/controllers/refresh.py +++ b/subiquity/controllers/refresh.py @@ -13,7 +13,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import enum import logging +import os + +import requests.exceptions from subiquitycore.controller import BaseController from subiquitycore.core import Skip @@ -21,10 +25,92 @@ from subiquitycore.core import Skip log = logging.getLogger('subiquity.controllers.refresh') +class CheckState(enum.IntEnum): + NOT_STARTED = enum.auto() + CHECKING = enum.auto() + FAILED = enum.auto() + + AVAILABLE = enum.auto() + UNAVAILABLE = enum.auto() + + def is_definite(self): + return self in [self.AVAILABLE, self.UNAVAILABLE] + + class RefreshController(BaseController): - def default(self, index=1): - raise Skip() + signals = [ + ('snapd-network-change', 'snapd_network_changed'), + ] - def cancel(self): + def __init__(self, common): + super().__init__(common) + self.snap_name = os.environ.get("SNAP_NAME", "subiquity") + self.check_state = CheckState.NOT_STARTED + self.view = None + self.offered_first_time = False + + def snapd_network_changed(self): + # If we restarted into this version, don't check for a new version. + if self.updated: + return + # If we got an answer, don't check again. + if self.check_state.is_definite(): + return + self.check_state = CheckState.CHECKING + self.run_in_bg(self._bg_check_for_update, self._check_result) + + def _bg_check_for_update(self): + return self.snapd_connection.get('v2/find', select='refresh') + + def _check_result(self, fut): + # If we managed to send concurrent requests and one has + # already provided an answer, just forget all about the other + # one! + if self.check_state.is_definite(): + return + try: + response = fut.result() + response.raise_for_status() + except requests.exceptions.RequestException as e: + log.exception("checking for update") + self.check_state = CheckState.FAILED + return + result = response.json() + log.debug("_check_result %s", result) + for snap in result["result"]: + if snap["name"] == self.snap_name: + self.check_state = CheckState.AVAILABLE + break + else: + self.check_state = CheckState.UNAVAILABLE + if self.view: + self.view.update_check_state() + + def default(self, index=1): + from subiquity.ui.views.refresh import RefreshView + if self.updated: + raise Skip() + show = False + if index == 1: + if self.check_state == CheckState.AVAILABLE: + show = True + self.offered_first_time = True + elif index == 2: + if not self.offered_first_time: + if self.check_state in [CheckState.AVAILABLE, + CheckState.CHECKING]: + show = True + else: + raise AssertionError("unexpected index {}".format(index)) + if show: + self.view = RefreshView(self) + self.ui.set_body(self.view) + else: + raise Skip() + + def done(self, sender=None): + self.signal.emit_signal('next-screen') + + def cancel(self, sender=None): self.signal.emit_signal('prev-screen') diff --git a/subiquity/snapd.py b/subiquity/snapd.py index 344cc723..394a066d 100644 --- a/subiquity/snapd.py +++ b/subiquity/snapd.py @@ -83,6 +83,7 @@ class FakeSnapdConnection: time.sleep(2) def get(self, path, **args): + log.debug("snapd get %s %s", path, args) filename = path.replace('/', '-') if args: filename += '-' + urlencode(sorted(args.items())) diff --git a/subiquity/ui/views/refresh.py b/subiquity/ui/views/refresh.py new file mode 100644 index 00000000..6c9fbdb6 --- /dev/null +++ b/subiquity/ui/views/refresh.py @@ -0,0 +1,152 @@ +# Copyright 2019 Canonical, Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging + +from urwid import ( + Text, + ) + +from subiquitycore.view import BaseView +from subiquitycore.ui.buttons import done_btn, other_btn +from subiquitycore.ui.utils import button_pile, screen + +from subiquity.controllers.refresh import CheckState +from subiquity.ui.spinner import Spinner + +log = logging.getLogger('subiquity.ui.views.refresh') + + +class RefreshView(BaseView): + + checking_title = _("Checking for installer update...") + checking_excerpt = _( + "Contacting the snap store to check if a new version of the " + "installer is available." + ) + + failed_title = _("Contacting the snap store failed") + failed_excerpt = _( + "Contacting the snap store failed:" + ) + + available_title = _("Installer update available") + available_excerpt = _( + "A new version of the installer is available." + ) + + progress_title = _("Downloading update...") + progress_excerpt = _( + "Please wait while the updated installer is being downloaded. The " + "installer will restart automatically when the download is complete." + ) + + def __init__(self, controller): + self.controller = controller + self.spinner = Spinner(self.controller.loop, style="dots") + + if self.controller.check_state == CheckState.CHECKING: + 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)) + + 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() + + rows = [self.spinner] + + buttons = [ + done_btn(_("Continue without updating"), on_press=self.done), + other_btn(_("Back"), on_press=self.cancel), + ] + + self.title = self.checking_title + self.controller.ui.set_header(self.title) + self._w = screen(rows, buttons, excerpt=_(self.checking_excerpt)) + + def check_state_available(self, sender=None): + self.spinner.stop() + + rows = [ + Text( + _("If you choose to update, the update will be downloaded " + "and the installation will continue from here."), + ) + ] + + buttons = button_pile([ + done_btn(_("Update to the new installer"), on_press=self.update), + 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.available_title + self.controller.ui.set_header(self.available_title) + self._w = screen(rows, buttons, excerpt=_(self.available_excerpt)) + + def check_state_failed(self): + self.spinner.stop() + + rows = [Text("")] + + buttons = button_pile([ + done_btn(_("Try again"), on_press=self.still_checking), + 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 update(self, sender=None): + self.spinner.stop() + + rows = [Text("not yet")] + + buttons = [ + other_btn(_("Cancel update"), on_press=self.check_state_available), + ] + + self.controller.ui.set_header("Downloading update...") + self._w = screen(rows, buttons, excerpt=_(self.progress_excerpt)) + # self.controller.start_update(self.update_started) + + def done(self, result=None): + self.spinner.stop() + self.controller.done() + + def cancel(self, result=None): + self.spinner.stop() + self.controller.cancel() diff --git a/subiquitycore/controller.py b/subiquitycore/controller.py index 1ec93a5f..a11382f1 100644 --- a/subiquitycore/controller.py +++ b/subiquitycore/controller.py @@ -38,6 +38,8 @@ class BaseController(ABC): self.input_filter = common['input_filter'] self.scale_factor = common['scale_factor'] self.run_in_bg = common['run_in_bg'] + self.updated = common['updated'] + self.application = common['application'] if 'snapd_connection' in common: self.snapd_connection = common['snapd_connection'] diff --git a/subiquitycore/core.py b/subiquitycore/core.py index 48e1a700..56cc95e3 100644 --- a/subiquitycore/core.py +++ b/subiquitycore/core.py @@ -277,6 +277,7 @@ class Application: scale = float(os.environ.get('SUBIQUITY_REPLAY_TIMESCALE', "1")) updated = os.path.exists(os.path.join(self.state_dir, 'updating')) self.common = { + "application": self, "updated": updated, "ui": ui, "opts": opts, diff --git a/subiquitycore/signals.py b/subiquitycore/signals.py index 681900f5..9065ddb0 100644 --- a/subiquitycore/signals.py +++ b/subiquitycore/signals.py @@ -54,8 +54,8 @@ class Signal: raise SignalException( "Passed something other than a required list.") for sig, cb in signal_callback: - if sig not in self.known_signals: - self.register_signals(sig) + # if sig not in self.known_signals: + self.register_signals(sig) self.connect_signal(sig, cb) def __repr__(self):