[master][draftPATCH] Handle packaging exits in a better way

Vratislav Podzimek vpodzime at redhat.com
Mon Oct 14 12:57:05 UTC 2013


For various reasons we shouldn't call sys.exit() from a non-main thread. But
since we need some packaging threads to stop right in the middle and deep in the
stack, the only way that works the same as sys.exit() is raising an exception.
Special exception that is caught and "translated" into a sys.exit() call from
the main thread.

This way, packaging doesn't need to have a clue about our queues and exit
handling.

Signed-off-by: Vratislav Podzimek <vpodzime at redhat.com>
---
 pyanaconda/install.py                | 11 ++++++++--
 pyanaconda/packaging/__init__.py     | 39 ++++++++++++++++++++++++++++++++++--
 pyanaconda/packaging/dnfpayload.py   | 14 +++----------
 pyanaconda/packaging/yumpayload.py   | 18 +++++------------
 pyanaconda/ui/communication.py       |  1 +
 pyanaconda/ui/gui/spokes/software.py | 12 +++++++++--
 pyanaconda/ui/tui/__init__.py        |  3 +++
 pyanaconda/ui/tui/spokes/software.py |  8 +++++++-
 8 files changed, 75 insertions(+), 31 deletions(-)

diff --git a/pyanaconda/install.py b/pyanaconda/install.py
index 0742d6f..8f05629 100644
--- a/pyanaconda/install.py
+++ b/pyanaconda/install.py
@@ -23,6 +23,7 @@
 from pyanaconda.constants import ROOT_PATH
 from blivet import turnOnFilesystems
 from pyanaconda.bootloader import writeBootLoader
+from pyanaconda.packaging import CannotContinueError
 from pyanaconda.progress import progress_report, progressQ
 from pyanaconda.users import createLuserConf, getPassAlgo, Users
 from pyanaconda import flags
@@ -158,8 +159,14 @@ def doInstall(storage, payload, ksdata, instClass):
 
     # don't try to install packages from the install class' ignored list
     packages = [p for p in packages if p not in instClass.ignoredPackages]
-    payload.preInstall(packages=packages, groups=payload.languageGroups())
-    payload.install()
+    try:
+        payload.preInstall(packages=packages, groups=payload.languageGroups())
+        payload.install()
+    except CannotContinueError:
+        # payload cannot continue, send quit message and return from the
+        # function so that the thread it runs in exits
+        progressQ.send_quit(1)
+        return
 
     if flags.flags.livecdInstall:
         storage.write()
diff --git a/pyanaconda/packaging/__init__.py b/pyanaconda/packaging/__init__.py
index 74adf7f..6117dba 100644
--- a/pyanaconda/packaging/__init__.py
+++ b/pyanaconda/packaging/__init__.py
@@ -70,6 +70,15 @@ urlgrabber.grabber.default_grabber.opts.user_agent = "%s (anaconda)/%s" %(produc
 class PayloadError(Exception):
     pass
 
+class CannotContinueError(PayloadError):
+    """
+    Exception that is raised when payload cannot continue and just wants to wait
+    for the process to exit.
+
+    """
+
+    pass
+
 class MetadataError(PayloadError):
     pass
 
@@ -390,6 +399,18 @@ class Payload(object):
 
         return self._kernelVersionList
 
+    def checkSoftwareSelection(self):
+        """
+        Check if selected software can be installed or not.
+
+        :raise CannotContinueError: if there is some fatal error and payload
+                                    cannot continue (e.g. user chooses not to
+                                    continue with some package missing/broken)
+
+        """
+
+        pass
+
     ##
     ## METHODS FOR TREE VERIFICATION
     ##
@@ -535,11 +556,25 @@ class Payload(object):
     ### METHODS FOR INSTALLING THE PAYLOAD
     ###
     def preInstall(self, packages=None, groups=None):
-        """ Perform pre-installation tasks. """
+        """
+        Perform pre-installation tasks.
+
+        :raise CannotContinueError: if there is some fatal error and payload
+                                    cannot continue
+
+        """
+
         iutil.mkdirChain(ROOT_PATH + "/root")
 
     def install(self):
-        """ Install the payload. """
+        """
+        Install the payload.
+
+        :raise CannotContinueError: if there is some fatal error and payload
+                                    cannot continue
+
+        """
+
         raise NotImplementedError()
 
     def _copyDriverDiskFiles(self):
diff --git a/pyanaconda/packaging/dnfpayload.py b/pyanaconda/packaging/dnfpayload.py
index d0bda57..f89306d 100644
--- a/pyanaconda/packaging/dnfpayload.py
+++ b/pyanaconda/packaging/dnfpayload.py
@@ -53,11 +53,6 @@ REPO_DIRS = ['/etc/yum.repos.d',
              '/tmp/updates/anaconda.repos.d',
              '/tmp/product/anaconda.repos.d']
 
-def _failure_limbo():
-    progressQ.send_quit(1)
-    while True:
-        time.sleep(10000)
-
 class PayloadRPMDisplay(dnf.output.LoggingTransactionDisplay):
     def __init__(self, queue):
         super(PayloadRPMDisplay, self).__init__()
@@ -177,10 +172,7 @@ class DNFPayload(packaging.PackagePayload):
         if errors.errorHandler.cb(exn, str(exn)) == errors.ERROR_RAISE:
             # The progress bar polls kind of slowly, thus installation could
             # still continue for a bit before the quit message is processed.
-            # Doing a sys.exit also ensures the running thread quits before
-            # it can do anything else.
-            progressQ.send_quit(1)
-            sys.exit(1)
+            raise packaging.CannotContinueError()
 
     def _select_group(self, group_id, default=True, optional=False):
         grp = self._base.comps.group_by_pattern(group_id)
@@ -339,7 +331,7 @@ class DNFPayload(packaging.PackagePayload):
             self.checkSoftwareSelection()
         except packaging.DependencyError:
             if errors.errorHandler.cb(e) == errors.ERROR_RAISE:
-                _failure_limbo()
+                raise packaging.CannotContinueError()
 
         pkgs_to_download = self._base.transaction.install_set
         log.info('Downloading pacakges.')
@@ -362,7 +354,7 @@ class DNFPayload(packaging.PackagePayload):
             (token, msg) = queue.get()
 
         if token == 'quit':
-            _failure_limbo()
+            raise packaging.CannotContinueError()
 
         post_msg = _("Performing post-installation setup tasks")
         progressQ.send_message(post_msg)
diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py
index 0eb5cf1..5a13310 100644
--- a/pyanaconda/packaging/yumpayload.py
+++ b/pyanaconda/packaging/yumpayload.py
@@ -76,7 +76,7 @@ import blivet.arch
 from pyanaconda.errors import ERROR_RAISE, errorHandler
 from pyanaconda.packaging import DependencyError, MetadataError, NoNetworkError, NoSuchGroup, \
                                  NoSuchPackage, PackagePayload, PayloadError, PayloadInstallError, \
-                                 PayloadSetupError
+                                 PayloadSetupError, CannotContinueError
 from pyanaconda.progress import progressQ
 
 from pyanaconda.localization import langcode_matches_locale
@@ -1137,11 +1137,7 @@ reposdir=%s
         if errorHandler.cb(exn, str(exn)) == ERROR_RAISE:
             # The progress bar polls kind of slowly, thus installation could
             # still continue for a bit before the quit message is processed.
-            # Let's sleep forever to prevent any further actions and wait for
-            # the main thread to quit the process.
-            progressQ.send_quit(1)
-            while True:
-                time.sleep(100000)
+            raise CannotContinueError()
 
     def _applyYumSelections(self):
         """ Apply the selections in ksdata to yum.
@@ -1311,9 +1307,7 @@ reposdir=%s
             self.checkSoftwareSelection()
         except DependencyError as e:
             if errorHandler.cb(e) == ERROR_RAISE:
-                progressQ.send_quit(1)
-                while True:
-                    time.sleep(100000)
+                raise CannotContinueError()
 
         # doPreInstall
         # create mountpoints for protected device mountpoints (?)
@@ -1423,8 +1417,7 @@ reposdir=%s
             log.error("Error running anaconda-yum: %s", e)
             exn = PayloadInstallError(str(e))
             if errorHandler.cb(exn) == ERROR_RAISE:
-                progressQ.send_quit(1)
-                sys.exit(1)
+                raise CannotContinueError()
         finally:
             # log the contents of the scriptlet logfile if any
             if os.path.exists(script_log):
@@ -1438,8 +1431,7 @@ reposdir=%s
         if install_errors:
             exn = PayloadInstallError("\n".join(install_errors))
             if errorHandler.cb(exn) == ERROR_RAISE:
-                progressQ.send_quit(1)
-                sys.exit(1)
+                raise CannotContinueError()
 
     def writeMultiLibConfig(self):
         if not self.data.packages.multiLib:
diff --git a/pyanaconda/ui/communication.py b/pyanaconda/ui/communication.py
index 6b819c5..9474829 100644
--- a/pyanaconda/ui/communication.py
+++ b/pyanaconda/ui/communication.py
@@ -38,3 +38,4 @@ hubQ.addMessage("message", 2)           # spoke_name, string
 hubQ.addMessage("input", 1)             # string
 hubQ.addMessage("exception", 1)         # exception
 hubQ.addMessage("show_message", 3)      # show_message_function, args, result_queue
+hubQ.addMessage("quit", 1)              # exit_code
diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py
index f4b7995..f39edd2 100644
--- a/pyanaconda/ui/gui/spokes/software.py
+++ b/pyanaconda/ui/gui/spokes/software.py
@@ -19,11 +19,11 @@
 # Red Hat Author(s): Chris Lumens <clumens at redhat.com>
 #
 
-from gi.repository import Gdk
+from gi.repository import Gdk, GLib
 
 from pyanaconda.flags import flags
 from pyanaconda.i18n import _, N_
-from pyanaconda.packaging import MetadataError
+from pyanaconda.packaging import MetadataError, CannotContinueError
 from pyanaconda.threads import threadMgr, AnacondaThread
 from pyanaconda import constants
 
@@ -37,6 +37,9 @@ import sys
 
 __all__ = ["SoftwareSelectionSpoke"]
 
+import logging
+log = logging.getLogger("anaconda")
+
 class SoftwareSelectionSpoke(NormalSpoke):
     builderObjects = ["addonStore", "environmentStore", "softwareWindow"]
     mainWidgetName = "softwareWindow"
@@ -106,6 +109,11 @@ class SoftwareSelectionSpoke(NormalSpoke):
             self._errorMsgs = "\n".join(sorted(e.message))
             hubQ.send_message(self.__class__.__name__, _("Error checking software dependencies"))
             self._tx_id = None
+        except CannotContinueError:
+            # cannot continue (as per user request, exit the process from the
+            # main thread)
+            log.info("Payload cannot continue, exitting.")
+            GLib.idle_add(sys.exit, 1)
         else:
             self._errorMsgs = None
             self._tx_id = self.payload.txID
diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py
index 1b12fb9..1a91c1e 100644
--- a/pyanaconda/ui/tui/__init__.py
+++ b/pyanaconda/ui/tui/__init__.py
@@ -143,6 +143,9 @@ class TextUserInterface(ui.UserInterface):
         self._app.register_event_handler(hubQ.HUB_CODE_EXCEPTION, exception_msg_handler)
         self._app.register_event_handler(hubQ.HUB_CODE_SHOW_MESSAGE, self._handle_show_message)
 
+        # exit on exit request with exit code specified as the first item in event data (ev[1][0])
+        self._app.register_event_handler(hubQ.HUB_CODE_QUIT, lambda ev, d: sys.exit(ev[1][0]))
+
         _hubs = self._list_hubs()
 
         # First, grab a list of all the standalone spokes.
diff --git a/pyanaconda/ui/tui/spokes/software.py b/pyanaconda/ui/tui/spokes/software.py
index 9b37f55..47e5d70 100644
--- a/pyanaconda/ui/tui/spokes/software.py
+++ b/pyanaconda/ui/tui/spokes/software.py
@@ -20,10 +20,11 @@
 #
 
 from pyanaconda.flags import flags
+from pyanaconda.ui.communication import hubQ
 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 MetadataError, DependencyError, CannotContinueError
 from pyanaconda.i18n import _
 
 from pyanaconda.constants import THREAD_PAYLOAD, THREAD_PAYLOAD_MD
@@ -32,6 +33,8 @@ from pyanaconda.constants_text import INPUT_PROCESSED
 
 __all__ = ["SoftwareSpoke"]
 
+import logging
+log = logging.getLogger("anaconda")
 
 class SoftwareSpoke(NormalTUISpoke):
     """ Spoke used to read new value of text to represent source repo. """
@@ -200,6 +203,9 @@ class SoftwareSpoke(NormalTUISpoke):
             self.payload.checkSoftwareSelection()
         except DependencyError:
             self._tx_id = None
+        except CannotContinueError:
+            log.info("Payload cannot continue, exitting.")
+            hubQ.send_quit(1)
         else:
             self._tx_id = self.payload.txID
 
-- 
1.7.11.7



More information about the anaconda-patches mailing list