[PATCH] Add support for autostep and --autoscreenshot (#1234896)

Martin Kolman mkolman at redhat.com
Tue Jul 28 15:03:24 UTC 2015


Add the missing support for the autostep [--autoscreenshot]
kickstart command to Anaconda. If just autostep is specified, Anaconda will
iterate over all active spokes just before leaving a hub.
If the --autoscreenshot option specified, it will also take a screenshot of
all hubs and of each spoke.

Also turn the take-screenshot code into a proper function that can
be called when auto-screenshotting and remove the old copy-screenshot
post-script (the last batch of screenshots is taken after the
postscripts have run, so screenshot copying is now handled
elsewhere).

Resolves: rhbz#1234896
Signed-off-by: Martin Kolman <mkolman at redhat.com>
---
 pyanaconda/constants.py              |   4 +
 pyanaconda/iutil.py                  |  27 ++++++
 pyanaconda/ui/gui/__init__.py        | 172 +++++++++++++++++++++++++++++++----
 pyanaconda/ui/gui/hubs/__init__.py   |  55 +++++++++++
 pyanaconda/ui/gui/spokes/__init__.py |   9 ++
 pyanaconda/ui/gui/spokes/welcome.py  |   5 +-
 6 files changed, 253 insertions(+), 19 deletions(-)

diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py
index 807bf0d..d9ee11f 100644
--- a/pyanaconda/constants.py
+++ b/pyanaconda/constants.py
@@ -154,6 +154,10 @@ PW_ASCII_CHARS = string.digits + string.ascii_letters + string.punctuation + " "
 # Recognizing a tarfile
 TAR_SUFFIX = (".tar", ".tbz", ".tgz", ".txz", ".tar.bz2", "tar.gz", "tar.xz")
 
+# screenshots
+SCREENSHOTS_DIRECTORY = "/tmp/anaconda-screenshots"
+SCREENSHOTS_TARGET_DIRECTORY = "/root/anaconda-screenshots"
+
 # cmdline arguments that append instead of overwrite
 CMDLINE_APPEND = ["modprobe.blacklist"]
 
diff --git a/pyanaconda/iutil.py b/pyanaconda/iutil.py
index c827431..57baf90 100644
--- a/pyanaconda/iutil.py
+++ b/pyanaconda/iutil.py
@@ -27,6 +27,7 @@ import os.path
 import errno
 import subprocess
 import unicodedata
+import shutil
 import string
 import tempfile
 import types
@@ -36,6 +37,7 @@ import signal
 
 from pyanaconda.flags import flags
 from pyanaconda.constants import DRACUT_SHUTDOWN_EJECT, TRANSLATIONS_UPDATE_DIR, UNSUPPORTED_HW
+from pyanaconda.constants import SCREENSHOTS_DIRECTORY, SCREENSHOTS_TARGET_DIRECTORY
 from pyanaconda.regexes import URL_PARSE
 
 from pyanaconda.i18n import _
@@ -1191,3 +1193,28 @@ def get_platform_groupid():
 def parent_dir(directory):
     """Return the parent's path"""
     return "/".join(os.path.normpath(directory).split("/")[:-1])
+
+def sysroot_path(path):
+    """Make the given relative or absolute path "sysrooted"
+       :param str path: path to be sysrooted
+       :returns: sysrooted path
+       :rtype: str
+    """
+    return os.path.join(getSysroot(), path.lstrip(os.path.sep))
+
+def save_screenshots():
+    """Save screenshots to the installed system"""
+    if not os.path.exists(SCREENSHOTS_DIRECTORY):
+        # there are no screenshots to copy
+        return
+    target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
+    log.info("saving screenshots taken during the installation to: %s", target_path)
+    try:
+        # create the screenshots directory
+        mkdirChain(target_path)
+        # copy all screenshots
+        for filename in os.listdir(SCREENSHOTS_DIRECTORY):
+            shutil.copy(os.path.join(SCREENSHOTS_DIRECTORY, filename), target_path)
+
+    except OSError:
+        log.exception("saving screenshots to installed system failed")
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index 9251949..2169aad 100644
--- a/pyanaconda/ui/gui/__init__.py
+++ b/pyanaconda/ui/gui/__init__.py
@@ -27,10 +27,10 @@ from gi.repository import Gdk, Gtk, AnacondaWidgets, Keybinder, GdkPixbuf, GLib,
 
 from pyanaconda.i18n import _
 from pyanaconda.constants import IPMI_ABORTED
-from pyanaconda import product, iutil
+from pyanaconda import product, iutil, constants
 
 from pyanaconda.ui import UserInterface, common
-from pyanaconda.ui.gui.utils import gtk_action_wait, unbusyCursor
+from pyanaconda.ui.gui.utils import gtk_action_wait, gtk_call_once, unbusyCursor
 from pyanaconda import ihelp
 import os.path
 
@@ -109,7 +109,7 @@ class GUIObject(common.UIObject):
     helpFile = None
     translationDomain = "anaconda"
 
-    screenshots_directory = "/tmp/anaconda-screenshots"
+    handles_autostep = False
 
     def __init__(self, data):
         """Create a new UIObject instance, including loading its uiFile and
@@ -156,6 +156,14 @@ class GUIObject(common.UIObject):
         Keybinder.init()
         Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
 
+        self._automaticEntry = False
+        self._autostepRunning = False
+        self._autostepDone = False
+        self._autostepDoneCallback = None
+
+        # this indicates if the screen is the last spoke to be processed for a hub
+        self.lastAutostepSpoke = False
+
     def _findUIFile(self):
         path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/")
         dirs = path.split(":")
@@ -171,28 +179,119 @@ class GUIObject(common.UIObject):
         raise IOError("Could not load UI file '%s' for object '%s'" % (self.uiFile, self))
 
     def _handlePrntScreen(self, *args, **kwargs):
-        global _screenshotIndex
         global _last_screenshot_timestamp
         # as a single press of the assigned key generates
         # multiple callbacks, we need to skip additional
         # callbacks for some time once a screenshot is taken
         if (time.time() - _last_screenshot_timestamp) >= SCREENSHOT_DELAY:
-            # Make sure the screenshot directory exists.
-            if not os.access(self.screenshots_directory, os.W_OK):
-                os.makedirs(self.screenshots_directory)
-
-            fn = os.path.join(self.screenshots_directory,
-                              "screenshot-%04d.png" % _screenshotIndex)
-            root_window = Gdk.get_default_root_window()
-            pixbuf = Gdk.pixbuf_get_from_window(root_window, 0, 0,
-                                                root_window.get_width(),
-                                                root_window.get_height())
-            pixbuf.savev(fn, 'png', [], [])
-            log.info("screenshot nr. %d taken", _screenshotIndex)
-            _screenshotIndex += 1
+            self.take_screenshot()
             # start counting from the time the screenshot operation is done
             _last_screenshot_timestamp = time.time()
 
+    def take_screenshot(self, name=None):
+        """Take a screenshot of the whole screen (works even with multiple displays)
+
+        :param name: optional name for the screenshot that will be appended to the filename,
+                     after the standard prefix & screenshot number
+        :type name: str or NoneType
+        """
+        global _screenshotIndex
+        # Make sure the screenshot directory exists.
+        iutil.mkdirChain(constants.SCREENSHOTS_DIRECTORY)
+
+        if name is None:
+            screenshot_filename = "screenshot-%04d.png" % _screenshotIndex
+        else:
+            screenshot_filename = "screenshot-%04d-%s.png" % (_screenshotIndex, name)
+
+        fn = os.path.join(constants.SCREENSHOTS_DIRECTORY, screenshot_filename)
+
+        root_window = self.main_window.get_window()
+        pixbuf = Gdk.pixbuf_get_from_window(root_window, 0, 0,
+                                            root_window.get_width(),
+                                            root_window.get_height())
+        pixbuf.savev(fn, 'png', [], [])
+        log.info("%s taken", screenshot_filename)
+        _screenshotIndex += 1
+
+    @property
+    def automaticEntry(self):
+        """Report if the given GUIObject has been displayed under automatic control
+
+        This is needed for example for installations with an incomplete kickstart,
+        as we need to differentiate the automatic screenshot pass from the user
+        entering a spoke to manually configure things. We also need to skip applying
+        changes if the spoke is entered automatically.
+        """
+        return self._automaticEntry
+
+    @automaticEntry.setter
+    def automaticEntry(self, value):
+        self._automaticEntry = value
+
+    @property
+    def autostepRunning(self):
+        """Report if the GUIObject is currently running autostep"""
+        return self._autostepRunning
+
+    @autostepRunning.setter
+    def autostepRunning(self, value):
+        self._autostepRunning = value
+
+    @property
+    def autostepDone(self):
+        """Report if autostep for this GUIObject has been finished"""
+        return self._autostepDone
+
+    @autostepDone.setter
+    def autostepDone(self, value):
+        self._autostepDone = value
+
+    @property
+    def autostepDoneCallback(self):
+        """A callback to be run once autostep has been finished"""
+        return self._autostepDoneCallback
+
+    @autostepDoneCallback.setter
+    def autostepDoneCallback(self, callback):
+        self._autostepDoneCallback = callback
+
+    def autostep(self):
+        """Autostep through this graphical object and through
+        any graphical objects managed by it (such as through spokes for a hub)
+        """
+        # report that autostep is running to prevent another from starting
+        self.autostepRunning = True
+        # take a screenshot of the current graphical object
+        if self.data.autostep.autoscreenshot:
+            # as autostep is triggered just before leaving a screen,
+            # we can safely take a screenshot of the "parent" object at once
+            # without using idle_add
+            self.take_screenshot(self.__class__.__name__)
+        self._doAutostep()
+        # done
+        self.autostepRunning = False
+        self.autostepDone = True
+        self._doPostAutostep()
+
+        # run the autostep-done callback (if any)
+        if self.autostepDoneCallback:
+            self.autostepDoneCallback(self)
+
+    def _doPostAutostep(self):
+        """To be overridden by the given GUIObject sub-class with custom code
+        that brings the GUI from the autostepping mode back to the normal mode.
+        This usually means to "click" the continue button or its equivalent.
+        """
+        pass
+
+    def _doAutostep(self):
+        """To be overridden by the given GUIObject sub-class with customized
+        autostepping code - if needed
+        (this is for example used to step through spokes in a hub)
+        """
+        pass
+
     @property
     def window(self):
         """Return the object out of the GtkBuilder representation
@@ -410,6 +509,27 @@ class MainWindow(Gtk.Window):
         """
         self._setVisibleChild(spoke)
 
+        # autostep through the spoke if required
+        if spoke.automaticEntry:
+            # we need to use idle_add here to give GTK time to render the spoke
+            gtk_call_once(self._autostep_spoke, spoke)
+
+    def _autostep_spoke(self, spoke):
+        """Step through a spoke and make a screenshot if required.
+        If this is the last spoke to be autostepped on a hub return to
+        the hub so that we can proceed to the next one.
+        """
+        # it might be possible that autostep is specified, but autoscreenshot isn't
+        if spoke.data.autostep.autoscreenshot:
+            spoke.take_screenshot(spoke.__class__.__name__)
+
+        if spoke.autostepDoneCallback:
+            spoke.autostepDoneCallback(spoke)
+
+        # if this is the last spoke then return to hub
+        if spoke.lastAutostepSpoke:
+            self.returnToHub()
+
     def returnToHub(self):
         """Exit a spoke and return to a hub."""
         self._setVisibleChild(self._current_action)
@@ -753,11 +873,29 @@ class GraphicalUserInterface(UserInterface):
     ### SIGNAL HANDLING METHODS
     ###
     def _on_continue_clicked(self, win, user_data=None):
+        # Autostep needs to be triggered just before switching to the next screen
+        # (or before quiting the installation if there are no more screens) to be consistent
+        # in both fully automatic kickstart installation and for installation with an incomplete
+        # kickstart. Therefore we basically "hook" the continue-clicked signal, start autostepping
+        # and ignore any other continue-clicked signals until autostep is done.
+        # Once autostep finishes, it emits the appropriate continue-clicked signal itself,
+        # switching to the next screen (if any).
+        if self.data.autostep.seen and self._currentAction.handles_autostep:
+            if self._currentAction.autostepRunning:
+                log.debug("ignoring the continue-clicked signal - autostep is running")
+                return
+            elif not self._currentAction.autostepDone:
+                self._currentAction.autostep()
+                return
+
         if not win.get_may_continue():
             return
 
         # If we're on the last screen, clicking Continue quits.
         if len(self._actions) == 1:
+            # save the screenshots to the installed system before killing Anaconda
+            # (the kickstart post scripts run to early, so we need to copy the screenshots now)
+            iutil.save_screenshots()
             Gtk.main_quit()
             return
 
diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py
index ab68893..2563300 100644
--- a/pyanaconda/ui/gui/hubs/__init__.py
+++ b/pyanaconda/ui/gui/hubs/__init__.py
@@ -54,6 +54,8 @@ class Hub(GUIObject, common.Hub):
        additional standalone screens either before or after them.
     """
 
+    handles_autostep = True
+
     def __init__(self, data, storage, payload, instclass):
         """Create a new Hub instance.
 
@@ -91,6 +93,9 @@ class Hub(GUIObject, common.Hub):
 
         self._checker = None
 
+        self._spokesToStepIn = []
+        self._spokeAutostepIndex = 0
+
     def _createBox(self):
         from gi.repository import Gtk, AnacondaWidgets
         from pyanaconda.ui.gui.utils import setViewportBackground
@@ -353,6 +358,11 @@ class Hub(GUIObject, common.Hub):
     def spoke_done(self, spoke):
         spoke.visitedSinceApplied = True
 
+        # don't apply any actions if the spoke was visited automatically
+        if spoke.automaticEntry:
+            spoke.automaticEntry = False
+            return
+
         # Don't take visitedSinceApplied into account here.  It will always be
         # True from the line above.
         if spoke.changed and (not spoke.skipTo or (spoke.skipTo and spoke.applyOnSkip)):
@@ -384,3 +394,48 @@ class Hub(GUIObject, common.Hub):
         else:
             self.main_window.returnToHub()
 
+    def _doAutostep(self):
+        """Autostep through all spokes managed by this hub"""
+        log.info("autostepping through all spokes on hub %s", self.__class__.__name__)
+
+        # create a list of all spokes in reverse alphabetic order, we will pop() from it when
+        # processing all the spokes so the screenshots will actually be in alphabetic order
+        self._spokesToStepIn = list(reversed(sorted(self._spokes.values(), key=lambda x: x.__class__.__name__)))
+
+        # we can't just loop over all the spokes due to the asynchronous nature of GtkStack, so we start by
+        # autostepping to the first spoke, this will trigger a callback that steps to the next spoke,
+        # until we run out of unvisited spokes
+        self._autostepSpoke()
+
+    def _autostepSpoke(self):
+        """Process a single spoke, if no more spokes are available report autostep as finished for the hub."""
+        # do we have some spokes to work on ?
+        if self._spokesToStepIn:
+            # take one of them
+            spoke = self._spokesToStepIn.pop()
+
+            # increment the number of processed spokes
+            self._spokeAutostepIndex += 1
+
+            log.debug("stepping to spoke %s (%d/%d)", spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes))
+
+            # notify the spoke about the upcoming automatic entry and set a callback that will be called
+            # once the spoke has been successfully processed
+            spoke.automaticEntry = True
+            spoke.autostepDoneCallback = lambda x: self._autostepSpoke()
+
+            # if this is the last spoke, tell it to return to hub once processed
+            if self._spokesToStepIn == []:
+                spoke.lastAutostepSpoke = True
+            gtk_call_once(self._on_spoke_clicked, None, None, spoke)
+        else:
+            log.info("autostep for hub %s finished", self.__class__.__name__)
+            gtk_call_once(self._doPostAutostep)
+
+    def _doPostAutostep(self):
+        if self._spokesToStepIn:
+            # there are still spokes that need to be stepped in
+            return
+        # we are done, re-emit the continue clicked signal we "consumed" previously
+        # so that the Anaconda GUI can switch to the next screen (or quit)
+        self.window.emit("continue-clicked")
diff --git a/pyanaconda/ui/gui/spokes/__init__.py b/pyanaconda/ui/gui/spokes/__init__.py
index 1227d44..17866ee 100644
--- a/pyanaconda/ui/gui/spokes/__init__.py
+++ b/pyanaconda/ui/gui/spokes/__init__.py
@@ -21,6 +21,7 @@
 
 from pyanaconda.ui import common
 from pyanaconda.ui.gui import GUIObject
+from pyanaconda.ui.gui.utils import gtk_call_once
 from pyanaconda import ihelp
 
 __all__ = ["StandaloneSpoke", "NormalSpoke"]
@@ -28,6 +29,9 @@ __all__ = ["StandaloneSpoke", "NormalSpoke"]
 # Inherit abstract methods from common.StandaloneSpoke
 # pylint: disable=abstract-method
 class StandaloneSpoke(GUIObject, common.StandaloneSpoke):
+
+    handles_autostep = True
+
     def __init__(self, data, storage, payload, instclass):
         GUIObject.__init__(self, data)
         common.StandaloneSpoke.__init__(self, storage, payload, instclass)
@@ -38,6 +42,11 @@ class StandaloneSpoke(GUIObject, common.StandaloneSpoke):
     def _on_continue_clicked(self, win, user_data=None):
         self.apply()
 
+    def _doPostAutostep(self):
+        # we are done, re-emit the continue clicked signal we "consumed" previously
+        # so that the Anaconda GUI can switch to the next screen
+        gtk_call_once(self.window.emit, "continue-clicked")
+
 # Inherit abstract methods from common.NormalSpoke
 # pylint: disable=abstract-method
 class NormalSpoke(GUIObject, common.NormalSpoke):
diff --git a/pyanaconda/ui/gui/spokes/welcome.py b/pyanaconda/ui/gui/spokes/welcome.py
index 91b2b3a..028ad43 100644
--- a/pyanaconda/ui/gui/spokes/welcome.py
+++ b/pyanaconda/ui/gui/spokes/welcome.py
@@ -314,8 +314,9 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke):
     # Override the default in StandaloneSpoke so we can display the beta
     # warning dialog first.
     def _on_continue_clicked(self, window, user_data=None):
-        # Don't display the betanag dialog if this is the final release.
-        if not isFinal:
+        # Don't display the betanag dialog if this is the final release or
+        # when autostep has been requested as betanag breaks the autostep logic.
+        if not isFinal and not self.data.autostep.seen:
             dlg = self.builder.get_object("betaWarnDialog")
             with self.main_window.enlightbox(dlg):
                 rc = dlg.run()
-- 
2.4.3



More information about the anaconda-patches mailing list