From 4c92e2dd5381b84d87fea3a7c4880d716e77bb11 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 5 Dec 2019 23:34:24 +1300 Subject: [PATCH] make the snaplist code more async --- subiquity/controllers/snaplist.py | 70 +++++++++-------------- subiquity/snapd.py | 1 + subiquity/ui/views/snaplist.py | 93 ++++++++++++++++--------------- 3 files changed, 76 insertions(+), 88 deletions(-) diff --git a/subiquity/controllers/snaplist.py b/subiquity/controllers/snaplist.py index 062598a7..9149d545 100644 --- a/subiquity/controllers/snaplist.py +++ b/subiquity/controllers/snaplist.py @@ -37,80 +37,64 @@ class SnapdSnapInfoLoader: self.model = model self.store_section = store_section - self._running = False + self.main_task = None self.snap_list_fetched = False self.failed = False self.snapd = snapd self.pending_info_snaps = [] - self.ongoing = {} # {snap:[callbacks]} + self.tasks = {} # {snap:task} def start(self): - self._running = True log.debug("loading list of snaps") - schedule_task(self._start()) + self.main_task = schedule_task(self._start()) async def _start(self): - self.ongoing[None] = [] + task = self.tasks[None] = schedule_task(self._load_list()) + await task + self.pending_snaps = self.model.get_snap_list() + log.debug("fetched list of %s snaps", len(self.pending_snaps)) + while self.pending_snaps: + snap = self.pending_snaps.pop(0) + task = self.tasks[snap] = schedule_task( + self._fetch_info_for_snap(snap)) + await task + + async def _load_list(self): try: result = await self.snapd.get( 'v2/find', section=self.store_section) except requests.exceptions.RequestException: log.exception("loading list of snaps failed") self.failed = True - self._running = False - return - if not self._running: return self.model.load_find_data(result) self.snap_list_fetched = True - self.pending_snaps = self.model.get_snap_list() - log.debug("fetched list of %s snaps", len(self.model.get_snap_list())) - for cb in self.ongoing.pop(None): - cb() - while self.pending_snaps and self._running: - snap = self.pending_snaps.pop(0) - self.ongoing[snap] = [] - await self._fetch_info_for_snap(snap) def stop(self): - self._running = False + if self.main_task is not None: + self.main_task.cancel() async def _fetch_info_for_snap(self, snap): log.debug('starting fetch for %s', snap.name) try: - data = await self.snapd.get( - 'v2/find', name=snap.name) + data = await self.snapd.get('v2/find', name=snap.name) except requests.exceptions.RequestException: log.exception("loading snap info failed") # XXX something better here? return - if not self._running: - return log.debug('got data for %s', snap.name) self.model.load_info_data(data) - for cb in self.ongoing.pop(snap): - cb() - def get_snap_list(self, callback): - if self.snap_list_fetched: - callback() - elif None in self.ongoing: - self.ongoing[None].append(callback) - else: - self.start() - self.ongoing[None].append(callback) + def get_snap_list_task(self): + return self.tasks[None] - def get_snap_info(self, snap, callback): - if len(snap.channels) > 0: - callback() - return - if snap not in self.ongoing: + def get_snap_info_task(self, snap): + if snap not in self.tasks: if snap in self.pending_snaps: self.pending_snaps.remove(snap) - self.ongoing[snap] = [] - schedule_task(self._fetch_info_for_snap(snap)) - self.ongoing[snap].append(callback) + self.tasks[snap] = schedule_task(self._fetch_info_for_snap(snap)) + return self.tasks[snap] class SnapListController(BaseController): @@ -153,11 +137,11 @@ class SnapListController(BaseController): return self.ui.set_body(SnapListView(self.model, self)) - def get_snap_list(self, callback): - self.loader.get_snap_list(callback) + def get_snap_list_task(self): + return self.loader.get_snap_list_task() - def get_snap_info(self, snap, callback): - self.loader.get_snap_info(snap, callback) + def get_snap_info_task(self, snap): + return self.loader.get_snap_info_task(snap) def done(self, snaps_to_install): log.debug( diff --git a/subiquity/snapd.py b/subiquity/snapd.py index 5064280f..3e061e5b 100644 --- a/subiquity/snapd.py +++ b/subiquity/snapd.py @@ -146,6 +146,7 @@ class FakeSnapdConnection: "Don't know how to fake POST response to {}".format((path, args))) def get(self, path, **args): + time.sleep(1) filename = path.replace('/', '-') if args: filename += '-' + urlencode(sorted(args.items())) diff --git a/subiquity/ui/views/snaplist.py b/subiquity/ui/views/snaplist.py index 44feee92..a7b2194b 100644 --- a/subiquity/ui/views/snaplist.py +++ b/subiquity/ui/views/snaplist.py @@ -29,6 +29,7 @@ from urwid import ( Text, ) +from subiquitycore.async_helpers import schedule_task from subiquitycore.ui.buttons import ok_btn, cancel_btn, other_btn from subiquitycore.ui.container import ( Columns, @@ -313,36 +314,37 @@ class SnapCheckBox(CheckBox): self.snap = snap super().__init__(snap.name, on_state_change=self.state_change) - def load_info(self): - called = False - fi = None + def loaded(self): + if len(self.snap.channels) == 0: # or other indication of failure + ff = FetchingFailed(self, self.snap) + self.parent.show_overlay(ff, width=ff.width) + else: + cur_chan = None + if self.snap.name in self.parent.to_install: + cur_chan = self.parent.to_install[self.snap.name].channel + siv = SnapInfoView(self.parent, self.snap, cur_chan) + self.parent.show_screen(screen( + siv, + [other_btn( + label=_("Close"), + on_press=self.parent.show_main_screen)], + focus_buttons=False)) - def callback(): - nonlocal called - called = True - if fi is not None: - fi.close() - if len(self.snap.channels) == 0: # or other indication of failure - ff = FetchingFailed(self, self.snap) - self.parent.show_overlay(ff, width=ff.width) - else: - cur_chan = None - if self.snap.name in self.parent.to_install: - cur_chan = self.parent.to_install[self.snap.name].channel - siv = SnapInfoView(self.parent, self.snap, cur_chan) - self.parent.show_screen(screen( - siv, - [other_btn( - label=_("Close"), - on_press=self.parent.show_main_screen)], - focus_buttons=False)) - self.parent.controller.get_snap_info(self.snap, callback) - # If we didn't get callback synchronously, display a dialog - # while the info loads. - if not called: - fi = FetchingInfo( - self.parent, self.snap, self.parent.controller.loop) - self.parent.show_overlay(fi, width=fi.width) + async def wait(self, t, fi): + await t + fi.close() + self.loaded() + + def load_info(self): + t = self.parent.controller.get_snap_info_task(self.snap) + + if t.done(): + self.loaded() + return + fi = FetchingInfo( + self.parent, self.snap, self.parent.controller.loop) + self.parent.show_overlay(fi, width=fi.width) + schedule_task(self.wait(t, fi)) def keypress(self, size, key): if key.startswith("enter"): @@ -371,29 +373,30 @@ class SnapListView(BaseView): self.to_install = {} # {snap_name: (channel, is_classic)} self.load() - def load(self, sender=None): - spinner = None - called = False + def loaded(self): + snap_list = self.model.get_snap_list() + if len(snap_list) == 0: + self.offer_retry() + else: + self.make_main_screen(snap_list) + self.show_main_screen() - def callback(): - nonlocal called - called = True - if spinner is not None: - spinner.stop() - snap_list = self.model.get_snap_list() - if len(snap_list) == 0: - self.offer_retry() - else: - self.make_main_screen(snap_list) - self.show_main_screen() - self.controller.get_snap_list(callback) - if called: + async def _wait(self, t, spinner): + spinner.stop() + await t + self.loaded() + + def load(self, sender=None): + t = self.controller.get_snap_list_task() + if t.done(): + self.loaded() return spinner = Spinner(self.controller.loop, style='dots') spinner.start() self._w = screen( [spinner], [ok_btn(label=_("Continue"), on_press=self.done)], excerpt=_("Loading server snaps from store, please wait...")) + schedule_task(self._wait(t, spinner)) def offer_retry(self): self._w = screen(