From a7bcc7faf081054dc2ec20871c44e952335c50b0 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Fri, 18 Sep 2020 21:16:59 +1200 Subject: [PATCH] add a way to wait for something with notification after 0.1s use this for moving between screens, removing some crummy code from subquity/core.py --- subiquity/controller.py | 4 ++ subiquity/core.py | 103 ++++++++++++++-------------------- subiquity/models/subiquity.py | 15 ++++- subiquitycore/tui.py | 72 +++++++++++++++++++++--- 4 files changed, 124 insertions(+), 70 deletions(-) diff --git a/subiquity/controller.py b/subiquity/controller.py index ba20f668..180dd47c 100644 --- a/subiquity/controller.py +++ b/subiquity/controller.py @@ -29,6 +29,10 @@ from subiquitycore.tuicontroller import ( log = logging.getLogger("subiquity.controller") +class Confirm(Exception): + pass + + class SubiquityController(BaseController): autoinstall_key = None diff --git a/subiquity/core.py b/subiquity/core.py index 463cf0b4..0bf6f2e9 100644 --- a/subiquity/core.py +++ b/subiquity/core.py @@ -46,6 +46,7 @@ from subiquity.common.errorreport import ( ErrorReporter, ErrorReportKind, ) +from subiquity.controller import Confirm from subiquity.journald import journald_listen from subiquity.keycodes import ( DummyKeycodesFilter, @@ -331,38 +332,6 @@ class Subiquity(TuiApplication): self.show_progress_handle.cancel() self.show_progress_handle = None - def next_screen(self): - can_install = all(e.is_set() for e in self.base_model.install_events) - if can_install and not self.install_confirmed: - if self.interactive(): - log.debug("showing InstallConfirmation over %s", self.ui.body) - from subiquity.ui.views.installprogress import ( - InstallConfirmation, - ) - self._cancel_show_progress() - self.add_global_overlay( - InstallConfirmation(self.ui.body, self)) - else: - yes = _('yes') - no = _('no') - answer = no - if 'autoinstall' in self.kernel_cmdline: - answer = yes - else: - print(_("Confirmation is required to continue.")) - print(_("Add 'autoinstall' to your kernel command line to" - " avoid this")) - print() - prompt = "\n\n{} ({}|{})".format( - _("Continue with autoinstall?"), yes, no) - while answer != yes: - print(prompt) - answer = input() - self.confirm_install() - super().next_screen() - else: - super().next_screen() - def interactive(self): if not self.autoinstall_config: return True @@ -386,42 +355,56 @@ class Subiquity(TuiApplication): break super().select_initial_screen(index) - async def select_screen(self, new): + async def move_screen(self, increment, coro): + try: + await super().move_screen(increment, coro) + except Confirm: + if self.interactive(): + log.debug("showing InstallConfirmation over %s", self.ui.body) + from subiquity.ui.views.installprogress import ( + InstallConfirmation, + ) + self.add_global_overlay( + InstallConfirmation(self.ui.body, self)) + else: + yes = _('yes') + no = _('no') + answer = no + if 'autoinstall' in self.kernel_cmdline: + answer = yes + else: + print(_("Confirmation is required to continue.")) + print(_("Add 'autoinstall' to your kernel command line to" + " avoid this")) + print() + prompt = "\n\n{} ({}|{})".format( + _("Continue with autoinstall?"), yes, no) + while answer != yes: + print(prompt) + answer = input() + self.confirm_install() + self.next_screen() + + async def make_view_for_controller(self, new): + can_install = all(e.is_set() for e in self.base_model.install_events) + if can_install and not self.install_confirmed: + if new.model_name: + if not self.base_model.is_configured(new.model_name): + raise Confirm if new.interactive(): - self._cancel_show_progress() - if self.progress_showing: - shown_for = self.aio_loop.time() - self.progress_shown_time - remaining = 1.0 - shown_for - if remaining > 0.0: - self.aio_loop.call_later( - remaining, self.select_screen, new) - return - self.progress_showing = False - await super().select_screen(new) if new.answers: - new.run_answers() - elif self.autoinstall_config and not new.autoinstall_applied: - if self.interactive() and self.show_progress_handle is None: - self.ui.block_input = True - self.show_progress_handle = self.aio_loop.call_later( - 0.1, self._show_progress) - schedule_task(self._apply(new)) + self.aio_loop.call_soon(new.run_answers) + return await super().make_view_for_controller(new) else: + if self.autoinstall_config and not new.autoinstall_applied: + await new.apply_autoinstall_config() + new.autoinstall_applied = True new.configured() raise Skip - def _show_progress(self): - self.ui.block_input = False - self.progress_shown_time = self.aio_loop.time() - self.progress_showing = True + def show_progress(self): self.ui.set_body(self.controllers.InstallProgress.progress_view) - async def _apply(self, controller): - await controller.apply_autoinstall_config() - controller.autoinstall_applied = True - controller.configured() - self.next_screen() - def _network_change(self): self.signal.emit_signal('snapd-network-change') diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py index 58b71769..44508f22 100644 --- a/subiquity/models/subiquity.py +++ b/subiquity/models/subiquity.py @@ -130,8 +130,21 @@ class SubiquityModel: } def configured(self, model_name): - log.debug("model %s is configured", model_name) self._events[model_name].set() + if model_name in INSTALL_MODEL_NAMES: + unconfigured = { + mn for mn in INSTALL_MODEL_NAMES + if not self.is_configured(mn) + } + elif model_name in POSTINSTALL_MODEL_NAMES: + unconfigured = { + mn for mn in POSTINSTALL_MODEL_NAMES + if not self.is_configured(mn) + } + log.debug("model %s is configured, to go %s", model_name, unconfigured) + + def is_configured(self, model_name): + return self._events[model_name].is_set() def get_target_groups(self): command = ['chroot', self.target, 'getent', 'group'] diff --git a/subiquitycore/tui.py b/subiquitycore/tui.py index 04996c4e..21482804 100644 --- a/subiquitycore/tui.py +++ b/subiquitycore/tui.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import asyncio import inspect import logging import os @@ -49,6 +50,14 @@ def extend_dec_special_charmap(): }) +# When waiting for something of unknown duration, block the UI for at +# most this long before showing an indication of progress. +MAX_BLOCK_TIME = 0.1 +# If an indication of progress is shown, show it for at least this +# long to avoid excessive flicker in the UI. +MIN_SHOW_PROGRESS_TIME = 1.0 + + class TuiApplication(Application): make_ui = SubiquityCoreUI @@ -97,7 +106,7 @@ class TuiApplication(Application): before_hook() schedule_task(_run()) - async def select_screen(self, new): + async def make_view_for_controller(self, new): new.context.enter("starting UI") if self.opts.screens and new.name not in self.opts.screens: raise Skip @@ -111,10 +120,46 @@ class TuiApplication(Application): new.context.exit("(skipped)") raise else: - self.ui.set_body(view) self.cur_screen = new - with open(self.state_path('last-screen'), 'w') as fp: - fp.write(new.name) + with open(self.state_path('last-screen'), 'w') as fp: + fp.write(new.name) + return view + + async def _wait_with_indication(self, awaitable, show, hide=None): + """Wait for something but tell the user if it takes a while. + + When waiting for something that can take an unknown length of + time, we want to tell the user if it takes more than a moment + (defined as MAX_BLOCK_TIME) but make sure that we display any + indication for long enough that the UI is not flickering + incomprehensively (MIN_SHOW_PROGRESS_TIME). + """ + min_show_task = None + + def _show(): + self.ui.block_input = False + nonlocal min_show_task + min_show_task = self.aio_loop.create_task( + asyncio.sleep(MIN_SHOW_PROGRESS_TIME)) + show() + + self.ui.block_input = True + show_handle = self.aio_loop.call_later(MAX_BLOCK_TIME, _show) + try: + result = await awaitable + finally: + if min_show_task: + await min_show_task + if hide is not None: + hide() + else: + self.ui.block_input = False + show_handle.cancel() + + return result + + def show_progress(self): + raise NotImplementedError async def _move_screen(self, increment, coro): if coro is not None: @@ -129,24 +174,33 @@ class TuiApplication(Application): self.controllers.index += increment if self.controllers.index < 0: self.controllers.index = cur_index - return + return None if self.controllers.index >= len(self.controllers.instances): self.exit() - return + return None new = self.controllers.cur try: - await self.select_screen(new) + return await self.make_view_for_controller(new) except Skip: log.debug("skipping screen %s", new.name) continue + except Exception: + self.controllers.index = cur_index + raise else: return + async def move_screen(self, increment, coro): + view = await self._wait_with_indication( + self._move_screen(increment, coro), self.show_progress) + if view is not None: + self.ui.set_body(view) + def next_screen(self, coro=None): - self.aio_loop.create_task(self._move_screen(1, coro)) + self.aio_loop.create_task(self.move_screen(1, coro)) def prev_screen(self): - self.aio_loop.create_task(self._move_screen(-1, None)) + self.aio_loop.create_task(self.move_screen(-1, None)) def select_initial_screen(self, controller_index): for controller in self.controllers.instances[:controller_index]: