[PATCH 2/2] Use one thread for payload setup.

David Shea dshea at redhat.com
Fri Aug 22 16:25:38 UTC 2014


Add an object to send events for payload states so that we can have one
copy of the package payload setup code instead of four. This removes
THREAD_PAYLOAD_MD and some duplicated code in THREAD_SOFTWARE_WATCHER,
which also removes the problems of THREAD_PAYLOAD_MD and
THREAD_SOFTWARE_WATCHER potentially accessing the yum environments and
groups properties simultaneously and causing a very confusing crash.

In the payload classes, remove the updateBaseRepo and gatherRepoMetadata
calls from setup so that the remainder of setup can be run separately
from these other two methods. For classes that no longer had any code in
setup, remove the method.

Resolves: rhbz#1125927
---
 anaconda                                 |   5 +-
 pyanaconda/constants.py                  |   2 +-
 pyanaconda/packaging/__init__.py         | 202 +++++++++++++++++++++++++++++--
 pyanaconda/packaging/rpmostreepayload.py |   3 -
 pyanaconda/packaging/yumpayload.py       |   7 --
 pyanaconda/ui/gui/spokes/network.py      |  11 +-
 pyanaconda/ui/gui/spokes/software.py     |  67 +++++-----
 pyanaconda/ui/gui/spokes/source.py       |  92 ++++++--------
 pyanaconda/ui/gui/spokes/storage.py      |   1 -
 pyanaconda/ui/tui/spokes/software.py     |  29 +----
 pyanaconda/ui/tui/spokes/source.py       |  38 ++----
 11 files changed, 281 insertions(+), 176 deletions(-)

diff --git a/anaconda b/anaconda
index 55fe188..6f04672 100755
--- a/anaconda
+++ b/anaconda
@@ -1086,7 +1086,7 @@ if __name__ == "__main__":
     atexit.register(exitHandler, ksdata.reboot, anaconda.storage)
 
     from blivet import storageInitialize
-    from pyanaconda.packaging import payloadInitialize
+    from pyanaconda.packaging import payloadMgr
     from pyanaconda.network import networkInitialize, wait_for_connecting_NM_thread
     from pyanaconda.timezone import time_initialize
 
@@ -1103,7 +1103,8 @@ if __name__ == "__main__":
                                      args=(ksdata.timezone, anaconda.storage, anaconda.bootloader)))
 
     threadMgr.add(AnacondaThread(name=constants.THREAD_WAIT_FOR_CONNECTING_NM, target=wait_for_connecting_NM_thread, args=(ksdata,)))
-    threadMgr.add(AnacondaThread(name=constants.THREAD_PAYLOAD, target=payloadInitialize, args=(anaconda.storage, ksdata, anaconda.payload)))
+    payloadMgr.restartThread(anaconda.storage, ksdata, anaconda.payload,
+            fallback=not flags.automatedInstall)
 
     # check if geolocation should be enabled for this type of installation
     use_geolocation = True
diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py
index becad34..a16a79d 100644
--- a/pyanaconda/constants.py
+++ b/pyanaconda/constants.py
@@ -102,7 +102,7 @@ THREAD_CHECK_STORAGE = "AnaCheckStorageThread"
 THREAD_CUSTOM_STORAGE_INIT = "AnaCustomStorageInit"
 THREAD_WAIT_FOR_CONNECTING_NM = "AnaWaitForConnectingNMThread"
 THREAD_PAYLOAD = "AnaPayloadThread"
-THREAD_PAYLOAD_MD = "AnaPayloadMDThread"
+THREAD_PAYLOAD_RESTART = "AnaPayloadRestartThread"
 THREAD_INPUT_BASENAME = "AnaInputThread"
 THREAD_SYNC_TIME_BASENAME = "AnaSyncTime"
 THREAD_EXCEPTION_HANDLING_TEST = "AnaExceptionHandlingTest"
diff --git a/pyanaconda/packaging/__init__.py b/pyanaconda/packaging/__init__.py
index 17c3655..f7cefd8 100644
--- a/pyanaconda/packaging/__init__.py
+++ b/pyanaconda/packaging/__init__.py
@@ -34,20 +34,26 @@ from urlgrabber.grabber import URLGrabError
 import ConfigParser
 import shutil
 import time
+import threading
 
 if __name__ == "__main__":
     from pyanaconda import anaconda_log
     anaconda_log.init()
 
 from pyanaconda.constants import DRACUT_ISODIR, DRACUT_REPODIR, DD_ALL, DD_FIRMWARE, DD_RPMS, INSTALL_TREE, ISO_DIR, ROOT_PATH, \
-                                 THREAD_STORAGE, THREAD_WAIT_FOR_CONNECTING_NM
+                                 THREAD_STORAGE, THREAD_WAIT_FOR_CONNECTING_NM, THREAD_PAYLOAD, \
+                                 THREAD_PAYLOAD_RESTART
 from pyanaconda.flags import flags
+from pyanaconda.i18n import _, N_
 
 from pyanaconda import iutil
 from pyanaconda import isys
 from pyanaconda.iutil import ProxyString, ProxyStringError
 
 from pykickstart.constants import GROUP_ALL, GROUP_DEFAULT, GROUP_REQUIRED
+
+from pyanaconda.threads import threadMgr, AnacondaThread
+
 from pykickstart.parser import Group
 
 import logging
@@ -754,16 +760,196 @@ class PackagePayload(Payload):
     def rpmMacros(self, value):
         self._rpm_macros = value
 
-def payloadInitialize(storage, ksdata, payload):
-    from pyanaconda.threads import threadMgr
+class PayloadManager(object):
+    """Framework for starting and watching the payload thread.
 
-    threadMgr.wait(THREAD_STORAGE)
+       This class defines several states, and events can be triggered upon
+       reaching a state. Depending on whether a state has already been reached
+       when a listener is added, the event code may be run in either the
+       calling thread or the payload thread. The event code will block the
+       payload thread regardless, so try not to run anything that takes a long
+       time.
 
-    # FIXME: condition for cases where we don't want network
-    # (set and use payload.needsNetwork ?)
-    threadMgr.wait(THREAD_WAIT_FOR_CONNECTING_NM)
+       All states except STATE_ERROR are expected to happen linearly, and adding
+       a listener for a state that has already been reached or passed will
+       immediately trigger that listener. For example, if the payload thread is
+       currently in STATE_GROUP_MD, adding a listener for STATE_NETWORK will
+       immediately run the code being added for STATE_NETWORK.
 
-    payload.setup(storage)
+       The payload thread data should be accessed using the payloadMgr object,
+       and the running thread can be accessed using threadMgr with the
+       THREAD_PAYLOAD constant, if you need to wait for it or something. The
+       thread should be started using payloadMgr.restartThread.
+    """
+
+    STATE_START = 0
+    # Waiting on storage
+    STATE_STORAGE = 1
+    # Waiting on network
+    STATE_NETWORK = 2
+    # Downloading package metadata
+    STATE_PACKAGE_MD = 3
+    # Downloading group metadata
+    STATE_GROUP_MD = 4
+    # All done
+    STATE_FINISHED = 5
+
+    # Error
+    STATE_ERROR = -1
+
+    # Error strings
+    ERROR_SETUP = N_("Failed to set up installation source")
+    ERROR_MD = N_("Error downloading package metadata")
+    ERROR_SOURCE = N_("No installation source avaialble")
+
+    def __init__(self):
+        self._event_lock = threading.Lock()
+        self._event_listeners = {}
+        self._thread_state = self.STATE_START
+        self._error = None
+
+        # Initialize a list for each event state
+        for event_id in range(self.STATE_ERROR, self.STATE_FINISHED + 1):
+            self._event_listeners[event_id] = []
+
+    @property
+    def error(self):
+        return _(self._error)
+
+    def addListener(self, event_id, func):
+        """Add a listener for an event.
+
+           :param int event_id: The event to listen for, one of the EVENT_* constants
+           :param function func: An object to call when the event is reached
+        """
+
+        # Check that the event_id is valid
+        assert isinstance(event_id, int)
+        assert event_id <= self.STATE_FINISHED
+        assert event_id >= self.STATE_ERROR
+
+        # Add the listener inside the lock in case we need to run immediately,
+        # to make sure the listener isn't triggered twice
+        with self._event_lock:
+            self._event_listeners[event_id].append(func)
+
+            # If an error event was requested, run it if currently in an error state
+            if event_id == self.STATE_ERROR:
+                if event_id == self._thread_state:
+                    func()
+            # Otherwise, run if the requested event has already occurred
+            elif event_id <= self._thread_state:
+                func()
+
+    def restartThread(self, storage, ksdata, payload, fallback=False, checkmount=True):
+        """Start or restart the payload thread.
+
+           If a payload thread is already running, this method will block
+           until the previous thread finishes. This method doesn't lock on
+           anything, so don't try to start two at once.
+
+           :param blivet.Blivet storage: The blivet storage instance
+           :param kickstart.AnacondaKSHandler ksdata: The kickstart data instance
+           :param packaging.Payload payload: The payload instance
+           :param installclass.BaseInstallClass instClass: The install class instance
+        """
+
+        log.debug("Restarting payload thread")
+
+        # If a restart thread is already running, don't start a new one
+        if threadMgr.get(THREAD_PAYLOAD_RESTART):
+            return
+
+        # Launch a new thread so that this method can return immediately
+        threadMgr.add(AnacondaThread(name=THREAD_PAYLOAD_RESTART, target=self._restartThread,
+            args=(storage, ksdata, payload, fallback, checkmount)))
+
+    def _restartThread(self, storage, ksdata, payload, fallback, checkmount):
+        # Wait for the old thread to finish
+        threadMgr.wait(THREAD_PAYLOAD)
+
+        # Start a new payload thread
+        threadMgr.add(AnacondaThread(name=THREAD_PAYLOAD, target=self._runThread,
+            args=(storage, ksdata, payload, fallback, checkmount)))
+
+    def _setState(self, event_id):
+        # Update the current state
+        log.debug("Updating payload thread state: %d", event_id)
+        with self._event_lock:
+            # Update the state within the lock to avoid a race with listeners
+            # currently being added
+            self._thread_state = event_id
+
+            # Run any listeners for the new state
+            for func in self._event_listeners[event_id]:
+                func()
+
+    def _runThread(self, storage, ksdata, payload, fallback, checkmount):
+        # This is the thread entry
+        # Set the initial state
+        self._error = None
+        self._setState(self.STATE_START)
+
+        # Wait for storage
+        self._setState(self.STATE_STORAGE)
+        threadMgr.wait(THREAD_STORAGE)
+
+        # Wait for network
+        self._setState(self.STATE_NETWORK)
+        # FIXME: condition for cases where we don't want network
+        # (set and use payload.needsNetwork ?)
+        threadMgr.wait(THREAD_WAIT_FOR_CONNECTING_NM)
+
+        self._setState(self.STATE_PACKAGE_MD)
+        payload.setup(storage)
+
+        # If this is a non-package Payload, we're done
+        if not isinstance(payload, PackagePayload):
+            self._setState(self.STATE_FINISHED)
+            return
+
+        # Keep setting up package-based repositories
+        # Download package metadata
+        try:
+            payload.updateBaseRepo(fallback=fallback, checkmount=checkmount)
+        except (OSError, PayloadError) as e:
+            log.error("PayloadError: %s", e)
+            self._error = self.ERROR_SETUP
+            self._setState(self.STATE_ERROR)
+            return
+
+        # Gather the group data
+        self._setState(self.STATE_GROUP_MD)
+        payload.gatherRepoMetadata()
+        payload.release()
+
+        # Check if that failed
+        if not payload.baseRepo:
+            log.error("No base repo configured")
+            self._error = self.ERROR_MD
+            self._setState(self.STATE_ERROR)
+            return
+
+        try:
+            # Grabbing the list of groups could potentially take a long time the
+            # first time (yum does a lot of magic property stuff, some of which
+            # involves side effects like network access) so go ahead and grab
+            # them now. These are properties with side-effects, just accessing
+            # them will trigger yum.
+            # pylint: disable=pointless-statement
+            payload.environments
+            # pylint: disable=pointless-statement
+            payload.groups
+        except MetadataError as e:
+            log.error("MetadataError: %s", e)
+            self._error = self.ERROR_SOURCE
+            self._setState(self.STATE_ERROR)
+            return
+
+        self._setState(self.STATE_FINISHED)
+
+# Initialize the PayloadManager instance
+payloadMgr = PayloadManager()
 
 def show_groups(payload):
     #repo = ksdata.RepoData(name="anaconda", baseurl="http://cannonball/install/rawhide/os/")
diff --git a/pyanaconda/packaging/rpmostreepayload.py b/pyanaconda/packaging/rpmostreepayload.py
index 5635309..ae5ad08 100644
--- a/pyanaconda/packaging/rpmostreepayload.py
+++ b/pyanaconda/packaging/rpmostreepayload.py
@@ -44,9 +44,6 @@ class RPMOSTreePayload(ArchivePayload):
     def __init__(self, data):
         super(RPMOSTreePayload, self).__init__(data)
 
-    def setup(self, storage):
-        super(RPMOSTreePayload, self).setup(storage)
-
     @property
     def handlesBootloaderConfiguration(self):
         return True
diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py
index e97790f..5ba121a 100644
--- a/pyanaconda/packaging/yumpayload.py
+++ b/pyanaconda/packaging/yumpayload.py
@@ -199,13 +199,6 @@ class YumPayload(PackagePayload):
         self._writeYumConfig()
         self._setup = True
 
-        self.updateBaseRepo(fallback=not flags.automatedInstall)
-
-        # When setup is called, it's already in a separate thread. That thread
-        # will try to select groups right after this returns, so make sure we
-        # have group info ready.
-        self.gatherRepoMetadata()
-
     def _resetYum(self, root=None, keep_cache=False, releasever=None):
         """ Delete and recreate the payload's YumBase instance.
 
diff --git a/pyanaconda/ui/gui/spokes/network.py b/pyanaconda/ui/gui/spokes/network.py
index 9163b6a..2b59653 100644
--- a/pyanaconda/ui/gui/spokes/network.py
+++ b/pyanaconda/ui/gui/spokes/network.py
@@ -32,8 +32,8 @@
 from gi.repository import Gtk
 
 from pyanaconda.flags import can_touch_runtime_system
+from pyanaconda.flags import flags as anaconda_flags
 from pyanaconda.i18n import _, N_
-from pyanaconda import constants
 from pyanaconda.ui.communication import hubQ
 from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke, StandaloneSpoke
@@ -1416,12 +1416,9 @@ class NetworkStandaloneSpoke(StandaloneSpoke):
 
         log.debug("network standalone spoke (apply) payload: %s completed: %s", self.payload.baseRepo, self._now_available)
         if not self.payload.baseRepo and not self._initially_available and self._now_available:
-            from pyanaconda.packaging import payloadInitialize
-            from pyanaconda.threads import threadMgr, AnacondaThread
-
-            threadMgr.wait(constants.THREAD_PAYLOAD)
-
-            threadMgr.add(AnacondaThread(name=constants.THREAD_PAYLOAD, target=payloadInitialize, args=(self.storage, self.data, self.payload)))
+            from pyanaconda.packaging import payloadMgr
+            payloadMgr.restartThread(self.storage, self.data, self.payload,
+                    fallback=not anaconda_flags.automatedInstall)
 
         self.network_control_box.kill_nmce(msg="leaving standalone network spoke")
 
diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py
index ff4346c..b2ef839 100644
--- a/pyanaconda/ui/gui/spokes/software.py
+++ b/pyanaconda/ui/gui/spokes/software.py
@@ -23,7 +23,7 @@ from gi.repository import Gdk
 
 from pyanaconda.flags import flags
 from pyanaconda.i18n import _, N_
-from pyanaconda.packaging import MetadataError, PackagePayload
+from pyanaconda.packaging import PackagePayload, payloadMgr
 from pyanaconda.threads import threadMgr, AnacondaThread
 from pyanaconda import constants, iutil
 
@@ -69,6 +69,25 @@ class SoftwareSelectionSpoke(NormalSpoke):
         self._environmentStore = self.builder.get_object("environmentStore")
         self._addonStore = self.builder.get_object("addonStore")
 
+        # Register event listeners to update our status on payload events
+        payloadMgr.addListener(payloadMgr.STATE_PACKAGE_MD, self._downloading_package_md)
+        payloadMgr.addListener(payloadMgr.STATE_GROUP_MD, self._downloading_group_md)
+        payloadMgr.addListener(payloadMgr.STATE_FINISHED, self._payload_finished)
+        payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error)
+
+    # Payload event handlers
+    def _downloading_package_md(self):
+        hubQ.send_message(self.__class__.__name__, _("Downloading package metadata..."))
+
+    def _downloading_group_md(self):
+        hubQ.send_message(self.__class__.__name__, _("Downloading group metadata..."))
+
+    def _payload_finished(self):
+        self.environment = self.data.packages.environment
+
+    def _payload_error(self):
+        hubQ.send_message(self.__class__.__name__, payloadMgr.error)
+
     def _apply(self):
         row = self._get_selected_environment()
         if not row:
@@ -157,7 +176,7 @@ class SoftwareSelectionSpoke(NormalSpoke):
         # repo metadata from the mirror list, or we detected a DVD/CD.
 
         return (not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER) and
-                not threadMgr.get(constants.THREAD_PAYLOAD_MD) and
+                not threadMgr.get(constants.THREAD_PAYLOAD) and
                 not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and
                 self.payload.baseRepo is not None)
 
@@ -194,41 +213,19 @@ class SoftwareSelectionSpoke(NormalSpoke):
                       target=self._initialize))
 
     def _initialize(self):
-        hubQ.send_message(self.__class__.__name__, _("Downloading package metadata..."))
-
         threadMgr.wait(constants.THREAD_PAYLOAD)
 
-        hubQ.send_message(self.__class__.__name__, _("Downloading group metadata..."))
-
         self.environment = self.data.packages.environment
 
-        if flags.automatedInstall and self.data.packages.seen:
-            # We don't want to do a full refresh, just join the metadata thread
-            threadMgr.wait(constants.THREAD_PAYLOAD_MD)
-        else:
-            # Grabbing the list of groups could potentially take a long time
-            # at first (yum does a lot of magic property stuff, some of which
-            # involves side effects like network access.  We need to reference
-            # them here, outside of the main thread, to not block the UI.
-            try:
-                # pylint: disable-msg=W0104
-                self.payload.environments
-                # pylint: disable-msg=W0104
-                self.payload.groups
-            except MetadataError:
-                hubQ.send_message(self.__class__.__name__,
-                                  _("No installation source available"))
-                return
-
-            # And then having done all that slow downloading, we need to do the first
-            # refresh of the UI here so there's an environment selected by default.
-            # This happens inside the main thread by necessity.  We can't do anything
-            # that takes any real amount of time, or it'll block the UI from updating.
+        if not flags.automatedInstall or not self.data.packages.seen:
+            # having done all the slow downloading, we need to do the first refresh
+            # of the UI here so there's an environment selected by default.  This
+            # happens inside the main thread by necessity.  We can't do anything
+            # that takes any real amount of time, or it'll block the UI from
+            # updating.
             if not self._first_refresh():
                 return
 
-        self.payload.release()
-
         hubQ.send_ready(self.__class__.__name__, False)
 
         # If packages were provided by an input kickstart file (or some other means),
@@ -237,17 +234,13 @@ class SoftwareSelectionSpoke(NormalSpoke):
 
     @gtk_action_wait
     def _first_refresh(self):
-        try:
-            self.refresh()
-            return True
-        except MetadataError:
-            hubQ.send_message(self.__class__.__name__, _("No installation source available"))
-            return False
+        self.refresh()
+        return True
 
     def refresh(self):
         NormalSpoke.refresh(self)
 
-        threadMgr.wait(constants.THREAD_PAYLOAD_MD)
+        threadMgr.wait(constants.THREAD_PAYLOAD)
 
         self._environmentStore.clear()
         if self.environment not in self.payload.environments:
diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py
index d1ce6a1..1cd9afb 100644
--- a/pyanaconda/ui/gui/spokes/source.py
+++ b/pyanaconda/ui/gui/spokes/source.py
@@ -40,7 +40,7 @@ from pyanaconda.ui.gui.utils import enlightbox, gtk_action_wait
 from pyanaconda.iutil import ProxyString, ProxyStringError, cmp_obj_attrs
 from pyanaconda.ui.gui.utils import gtk_call_once, really_hide, really_show
 from pyanaconda.threads import threadMgr, AnacondaThread
-from pyanaconda.packaging import PayloadError, MetadataError, PackagePayload
+from pyanaconda.packaging import PackagePayload, payloadMgr
 from pyanaconda import constants
 
 from blivet.util import get_mount_device, get_mount_paths
@@ -49,7 +49,6 @@ __all__ = ["SourceSpoke"]
 
 BASEREPO_SETUP_MESSAGE = N_("Setting up installation source...")
 METADATA_DOWNLOAD_MESSAGE = N_("Downloading package metadata...")
-METADATA_ERROR_MESSAGE = N_("Error downloading package metadata...")
 
 # These need to be in the same order as the items in protocolComboBox in source.glade.
 PROTOCOL_HTTP = 0
@@ -305,7 +304,7 @@ class SourceSpoke(NormalSpoke):
         if flags.askmethod:
             flags.askmethod = False
 
-        threadMgr.add(AnacondaThread(name=constants.THREAD_PAYLOAD_MD, target=self.getRepoMetadata))
+        payloadMgr.restartThread(self.storage, self.data, self.payload, checkmount=False)
         self.clear_info()
 
     def _method_changed(self):
@@ -434,55 +433,6 @@ class SourceSpoke(NormalSpoke):
 
         return True
 
-    def getRepoMetadata(self):
-        hubQ.send_not_ready("SoftwareSelectionSpoke")
-        hubQ.send_not_ready(self.__class__.__name__)
-        hubQ.send_message(self.__class__.__name__, _(BASEREPO_SETUP_MESSAGE))
-        # this sleep is lame, but without it the message above doesn't seem
-        # to get processed by the hub in time, and is never shown.
-        # FIXME this should get removed when we figure out how to ensure
-        # that the message takes effect on the hub before we try to mount
-        # a bad NFS server.
-        time.sleep(1)
-        try:
-            self.payload.updateBaseRepo(fallback=False, checkmount=False)
-        except (OSError, PayloadError) as e:
-            log.error("PayloadError: %s", e)
-            self._error = True
-            hubQ.send_message(self.__class__.__name__, _("Failed to set up installation source"))
-            if not (hasattr(self.data.method, "proxy") and self.data.method.proxy):
-                gtk_call_once(self.set_warning, _("Failed to set up installation source; check the repo url"))
-            else:
-                gtk_call_once(self.set_warning, _("Failed to set up installation source; check the repo url and proxy settings"))
-        else:
-            self._error = False
-            hubQ.send_message(self.__class__.__name__, _(METADATA_DOWNLOAD_MESSAGE))
-            self.payload.gatherRepoMetadata()
-            self.payload.release()
-            if not self.payload.baseRepo:
-                hubQ.send_message(self.__class__.__name__, _(METADATA_ERROR_MESSAGE))
-                hubQ.send_ready(self.__class__.__name__, False)
-                self._error = True
-                gtk_call_once(self.set_warning, _("Failed to set up installation source; check the repo url"))
-            else:
-                try:
-                    # Grabbing the list of groups could potentially take a long time the
-                    # first time (yum does a lot of magic property stuff, some of which
-                    # involves side effects like network access) so go ahead and grab
-                    # them now. These are properties with side-effects, just accessing
-                    # them will trigger yum.
-                    # pylint: disable-msg=W0104
-                    self.payload.environments
-                    # pylint: disable-msg=W0104
-                    self.payload.groups
-                except MetadataError:
-                    hubQ.send_message("SoftwareSelectionSpoke",
-                                      _("No installation source available"))
-                else:
-                    hubQ.send_ready("SoftwareSelectionSpoke", False)
-        finally:
-            hubQ.send_ready(self.__class__.__name__, False)
-
     @property
     def changed(self):
         method_changed = self._method_changed()
@@ -506,7 +456,7 @@ class SourceSpoke(NormalSpoke):
     @property
     def ready(self):
         return (self._ready and
-                not threadMgr.get(constants.THREAD_PAYLOAD_MD) and
+                not threadMgr.get(constants.THREAD_PAYLOAD) and
                 not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER) and
                 not threadMgr.get(constants.THREAD_CHECK_SOFTWARE))
 
@@ -597,13 +547,45 @@ class SourceSpoke(NormalSpoke):
 
         threadMgr.add(AnacondaThread(name=constants.THREAD_SOURCE_WATCHER, target=self._initialize))
 
-    def _initialize(self):
+        # Register listeners for payload events
+        payloadMgr.addListener(payloadMgr.STATE_START, self._payload_refresh)
+        payloadMgr.addListener(payloadMgr.STATE_STORAGE, self._probing_storage)
+        payloadMgr.addListener(payloadMgr.STATE_GROUP_MD, self._downloading_package_md)
+        payloadMgr.addListener(payloadMgr.STATE_FINISHED, self._payload_finished)
+        payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error)
+
+    def _payload_refresh(self):
+        hubQ.send_not_ready("SoftwareSelectionSpoke")
+        hubQ.send_not_ready(self.__class__.__name__)
+        hubQ.send_message(self.__class__.__name__, _(BASEREPO_SETUP_MESSAGE))
+
+        # this sleep is lame, but without it the message above doesn't seem
+        # to get processed by the hub in time, and is never shown.
+        # FIXME this should get removed when we figure out how to ensure
+        # that the message takes effect on the hub before we try to mount
+        # a bad NFS server.
+        time.sleep(1)
+
+    def _probing_storage(self):
         hubQ.send_message(self.__class__.__name__, _("Probing storage..."))
 
-        threadMgr.wait(constants.THREAD_STORAGE)
+    def _downloading_package_md(self):
+        # Reset the error state from previous payloads
+        self._error = False
 
         hubQ.send_message(self.__class__.__name__, _(METADATA_DOWNLOAD_MESSAGE))
 
+    def _payload_finished(self):
+        hubQ.send_ready("SoftwareSelectionSpoke", False)
+        hubQ.send_ready(self.__class__.__name__, False)
+
+    def _payload_error(self):
+        self._error = True
+        hubQ.send_message(self.__class__.__name__, payloadMgr.error)
+        gtk_call_once(self.set_warning, _("Failed to set up installation source; check the repo url"))
+        hubQ.send_ready(self.__class__.__name__, False)
+
+    def _initialize(self):
         threadMgr.wait(constants.THREAD_PAYLOAD)
 
         added = False
diff --git a/pyanaconda/ui/gui/spokes/storage.py b/pyanaconda/ui/gui/spokes/storage.py
index 0f0d2d2..40bdf26 100644
--- a/pyanaconda/ui/gui/spokes/storage.py
+++ b/pyanaconda/ui/gui/spokes/storage.py
@@ -123,7 +123,6 @@ class InstallOptionsDialogBase(GUIObject):
     def _software_is_ready(self):
         # FIXME:  Would be nicer to just ask the spoke if it's ready.
         return (not threadMgr.get(constants.THREAD_PAYLOAD) and
-                not threadMgr.get(constants.THREAD_PAYLOAD_MD) and
                 not threadMgr.get(constants.THREAD_SOFTWARE_WATCHER) and
                 not threadMgr.get(constants.THREAD_CHECK_SOFTWARE) and
                 self.payload.baseRepo is not None)
diff --git a/pyanaconda/ui/tui/spokes/software.py b/pyanaconda/ui/tui/spokes/software.py
index d6e0be5..9ce2d94 100644
--- a/pyanaconda/ui/tui/spokes/software.py
+++ b/pyanaconda/ui/tui/spokes/software.py
@@ -23,11 +23,11 @@ from pyanaconda.flags import flags
 from pyanaconda.ui.tui.spokes import NormalTUISpoke
 from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget, CheckboxWidget
 from pyanaconda.threads import threadMgr, AnacondaThread
-from pyanaconda.packaging import MetadataError, DependencyError
+from pyanaconda.packaging import DependencyError
 from pyanaconda.i18n import N_, _
 
-from pyanaconda.constants import THREAD_PAYLOAD, THREAD_PAYLOAD_MD
-from pyanaconda.constants import THREAD_CHECK_SOFTWARE, THREAD_SOFTWARE_WATCHER
+from pyanaconda.constants import THREAD_PAYLOAD
+from pyanaconda.constants import THREAD_CHECK_SOFTWARE
 
 __all__ = ["SoftwareSpoke"]
 
@@ -39,7 +39,6 @@ class SoftwareSpoke(NormalTUISpoke):
 
     def __init__(self, app, data, storage, payload, instclass):
         NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
-        self._ready = False
         self.errors = []
         self._tx_id = None
         # default to first selection (Gnome) in list of environments
@@ -52,25 +51,6 @@ class SoftwareSpoke(NormalTUISpoke):
         # are we taking values (package list) from a kickstart file?
         self._kickstarted = flags.automatedInstall and self.data.packages.seen
 
-    def initialize(self):
-        NormalTUISpoke.initialize(self)
-        threadMgr.add(AnacondaThread(name=THREAD_SOFTWARE_WATCHER, target=self._initialize))
-
-    def _initialize(self):
-        """ Private initialize. """
-        threadMgr.wait(THREAD_PAYLOAD)
-        if self._kickstarted:
-            threadMgr.wait(THREAD_PAYLOAD_MD)
-        else:
-            try:
-                self.payload.environments
-            except MetadataError:
-                self.errors.append(_("No installation source available"))
-                return
-        self.payload.release()
-
-        self._ready = True
-
     @property
     def showable(self):
         return not flags.livecdInstall and not self.data.method.method == "liveimg"
@@ -175,8 +155,7 @@ class SoftwareSpoke(NormalTUISpoke):
     @property
     def ready(self):
         """ If we're ready to move on. """
-        return (not threadMgr.get(THREAD_SOFTWARE_WATCHER) and
-                not threadMgr.get(THREAD_PAYLOAD_MD) and
+        return (not threadMgr.get(THREAD_PAYLOAD) and
                 not threadMgr.get(THREAD_CHECK_SOFTWARE))
 
     def apply(self):
diff --git a/pyanaconda/ui/tui/spokes/source.py b/pyanaconda/ui/tui/spokes/source.py
index 5702291..c526579 100644
--- a/pyanaconda/ui/tui/spokes/source.py
+++ b/pyanaconda/ui/tui/spokes/source.py
@@ -25,13 +25,13 @@ from pyanaconda.ui.tui.spokes import EditTUISpoke, NormalTUISpoke
 from pyanaconda.ui.tui.spokes import EditTUISpokeEntry as Entry
 from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget
 from pyanaconda.threads import threadMgr, AnacondaThread
-from pyanaconda.packaging import PayloadError, MetadataError
+from pyanaconda.packaging import payloadMgr
 from pyanaconda.i18n import N_, _
 from pyanaconda.image import opticalInstallMedia, potentialHdisoSources
 from pyanaconda.iutil import DataHolder
 
-from pyanaconda.constants import THREAD_SOURCE_WATCHER, THREAD_SOFTWARE_WATCHER, THREAD_PAYLOAD
-from pyanaconda.constants import THREAD_PAYLOAD_MD, THREAD_STORAGE, THREAD_STORAGE_WATCHER
+from pyanaconda.constants import THREAD_SOURCE_WATCHER, THREAD_PAYLOAD
+from pyanaconda.constants import THREAD_STORAGE_WATCHER
 from pyanaconda.constants import THREAD_CHECK_SOFTWARE, ISO_DIR, DRACUT_ISODIR, DRACUT_REPODIR
 
 from blivet.util import get_mount_device, get_mount_paths
@@ -165,10 +165,10 @@ class SourceSpoke(SourceSwitchHandler, EditTUISpoke):
 
         threadMgr.add(AnacondaThread(name=THREAD_SOURCE_WATCHER,
                                      target=self._initialize))
+        payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error)
 
     def _initialize(self):
         """ Private initialize. """
-        threadMgr.wait(THREAD_STORAGE)
         threadMgr.wait(THREAD_PAYLOAD)
         # If we've previously set up to use a CD/DVD method, the media has
         # already been mounted by payload.setup.  We can't try to mount it
@@ -185,6 +185,9 @@ class SourceSpoke(SourceSwitchHandler, EditTUISpoke):
 
         self._ready = True
 
+    def _payload_error(self):
+        self.errors.append(payloadMgr.error)
+
     def _repo_status(self):
         """ Return a string describing repo url or lack of one. """
         if self.data.method.method == "url":
@@ -322,35 +325,11 @@ class SourceSpoke(SourceSwitchHandler, EditTUISpoke):
         except (ValueError, IndexError):
             return key
 
-    def getRepoMetadata(self):
-        """ Pull down yum repo metadata """
-        try:
-            self.payload.updateBaseRepo(fallback=False, checkmount=False)
-        except (OSError, PayloadError) as err:
-            LOG.error("Error: %s", err)
-            self.errors.append(_("Failed to set up installation source"))
-        else:
-            self.payload.gatherRepoMetadata()
-            self.payload.release()
-            if not self.payload.baseRepo:
-                self.errors.append(_("Error downloading package metadata"))
-            else:
-                try:
-                    # pylint: disable-msg=W0104
-                    self.payload.environments
-                    # pylint: disable-msg=W0104
-                    self.payload.groups
-                    self.errors = []
-                except MetadataError:
-                    self.errors.append(_("No installation source available"))
-
     @property
     def ready(self):
         """ Check if the spoke is ready. """
         return (self._ready and
                 not threadMgr.get(THREAD_PAYLOAD) and
-                not threadMgr.get(THREAD_PAYLOAD_MD) and
-                not threadMgr.get(THREAD_SOFTWARE_WATCHER) and
                 not threadMgr.get(THREAD_CHECK_SOFTWARE))
 
     def apply(self):
@@ -360,8 +339,7 @@ class SourceSpoke(SourceSwitchHandler, EditTUISpoke):
         if flags.askmethod:
             flags.askmethod = False
 
-        threadMgr.add(AnacondaThread(name=THREAD_PAYLOAD_MD,
-                      target=self.getRepoMetadata))
+        payloadMgr.restartThread(self.storage, self.data, self.payload, checkmount=False)
 
 class SpecifyRepoSpoke(SourceSwitchHandler, EditTUISpoke):
     """ Specify the repo URL here if closest mirror not selected. """
-- 
1.9.3



More information about the anaconda-patches mailing list