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@redhat.com --- pyanaconda/constants.py | 4 + pyanaconda/iutil.py | 27 ++++++ pyanaconda/ui/gui/__init__.py | 179 +++++++++++++++++++++++++++++++---- pyanaconda/ui/gui/hubs/__init__.py | 46 ++++++++- pyanaconda/ui/gui/spokes/__init__.py | 9 ++ pyanaconda/ui/gui/spokes/welcome.py | 5 +- 6 files changed, 250 insertions(+), 20 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..9136406 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..8ffc723 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,12 @@ class GUIObject(common.UIObject): Keybinder.init() Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
+ self._automaticEntry = False + self._autostepRunning = False + self._autostepDone = False + self._autostepDoneCallback = None + 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 +177,128 @@ 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 + + @property + def lastAutostepSpoke(self): + """Indicates if the screen is the last spoke to be processed on a hub""" + return self._lastAutostepSpoke + + @lastAutostepSpoke.setter + def lastAutostepSpoke(self, value): + self._lastAutostepSpoke = value + + 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 +516,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 +880,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..bb65329 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -27,7 +27,7 @@ from pyanaconda.product import distributionText
from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup +from pyanaconda.ui.gui.utils import gtk_call_once, fire_gtk_action, escape_markup
import logging log = logging.getLogger("anaconda") @@ -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,37 @@ 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__) + # created 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): + if self._spokesToStepIn: + spoke = self._spokesToStepIn.pop() + self._spokeAutostepIndex += 1 + log.debug("stepping to spoke %s (%d/%d)", spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes)) + spoke.automaticEntry = True + spoke.autostepDoneCallback = lambda x: self._autostepSpoke() + # this is the last spoke, tell it to return to hub once the screenshot is taken + if self._spokesToStepIn == []: + spoke.lastAutostepSpoke = True + #fire_gtk_action(self._on_spoke_clicked, None, None, spoke) + fire_gtk_action(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()
On Mon, 2015-07-27 at 18:55 +0200, Martin Kolman wrote:
diff --git a/pyanaconda/iutil.py b/pyanaconda/iutil.py index c827431..9136406 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 copyreturn- target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
- log.info("saving screenshots taken during the installation to:
%s" % target_path)
I think you should use ',' instead of '%'. So this way:
log.info("... installation to: %s", target_path)
- try:
# create the screenshots directorymkdirChain(target_path)# copy all screenshotsfor filename in os.listdir(SCREENSHOTS_DIRECTORY):shutil.copy(os.path.join(SCREENSHOTS_DIRECTORY,filename), target_path)
- except OSError:
log.exception("saving screenshots to installed systemfailed")
. . .
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index 9251949..8ffc723 100644 from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup +from pyanaconda.ui.gui.utils import gtk_call_once, fire_gtk_action, escape_markup
import logging log = logging.getLogger("anaconda") @@ -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 visitedautomatically
if spoke.automaticEntry:spoke.automaticEntry = Falsereturn# Don't take visitedSinceApplied into account here. It willalways be # True from the line above. if spoke.changed and (not spoke.skipTo or (spoke.skipTo and spoke.applyOnSkip)): @@ -384,3 +394,37 @@ 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__)
# created a list of all spokes in reverse alphabetic order,we will pop() from it when
# processing all the spokes so the screenshots will actuallybe 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 theasynchronous nature of GtkStack, so we start by
# autostepping to the first spoke, this will trigger acallback that steps to the next spoke,
# until we run out of unvisited spokesself._autostepSpoke()
Please make this readable by adding some blank lines here.
- def _autostepSpoke(self):
if self._spokesToStepIn:spoke = self._spokesToStepIn.pop()self._spokeAutostepIndex += 1log.debug("stepping to spoke %s (%d/%d)",spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes))
spoke.automaticEntry = Truespoke.autostepDoneCallback = lambda x:self._autostepSpoke()
# this is the last spoke, tell it to return to hub oncethe screenshot is taken
if self._spokesToStepIn == []:spoke.lastAutostepSpoke = True#fire_gtk_action(self._on_spoke_clicked, None, None,spoke)
fire_gtk_action(self._on_spoke_clicked, None, None,spoke)
else:log.info("autostep for hub %s finished",self.__class__.__name__)
gtk_call_once(self._doPostAutostep)
Same as before some blank lines please.
- def _doPostAutostep(self):
if self._spokesToStepIn:# there are still spokes that need to be stepped inreturn# we are done, re-emit the continue clicked signal we"consumed" previously
# so that the Anaconda GUI can switch to the next screen (orquit)
self.window.emit("continue-clicked")
Except the three above everything looks good to me.
On Tue, 2015-07-28 at 10:14 +0200, Jiří Konečný wrote:
On Mon, 2015-07-27 at 18:55 +0200, Martin Kolman wrote:
diff --git a/pyanaconda/iutil.py b/pyanaconda/iutil.py index c827431..9136406 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 copyreturn- target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
- log.info("saving screenshots taken during the installation to:
%s" % target_path)
I think you should use ',' instead of '%'. So this way:
log.info("... installation to: %s", target_path)
Yep, that's certainly wrong, thanks for spotting this. :-)
- try:
# create the screenshots directorymkdirChain(target_path)# copy all screenshotsfor filename in os.listdir(SCREENSHOTS_DIRECTORY):shutil.copy(os.path.join(SCREENSHOTS_DIRECTORY,filename), target_path)
- except OSError:
log.exception("saving screenshots to installed systemfailed")
. . .
diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index 9251949..8ffc723 100644 from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup +from pyanaconda.ui.gui.utils import gtk_call_once, fire_gtk_action, escape_markup
import logging log = logging.getLogger("anaconda") @@ -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 visitedautomatically
if spoke.automaticEntry:spoke.automaticEntry = Falsereturn# Don't take visitedSinceApplied into account here. Itwill always be # True from the line above. if spoke.changed and (not spoke.skipTo or (spoke.skipTo and spoke.applyOnSkip)): @@ -384,3 +394,37 @@ 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__)
# created a list of all spokes in reverse alphabeticorder, we will pop() from it when
# processing all the spokes so the screenshots willactually 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 theasynchronous nature of GtkStack, so we start by
# autostepping to the first spoke, this will trigger acallback that steps to the next spoke,
# until we run out of unvisited spokesself._autostepSpoke()Please make this readable by adding some blank lines here.
Sure!
- def _autostepSpoke(self):
if self._spokesToStepIn:spoke = self._spokesToStepIn.pop()self._spokeAutostepIndex += 1log.debug("stepping to spoke %s (%d/%d)",spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes))
spoke.automaticEntry = Truespoke.autostepDoneCallback = lambda x:self._autostepSpoke()
# this is the last spoke, tell it to return to hubonce the screenshot is taken
if self._spokesToStepIn == []:spoke.lastAutostepSpoke = True#fire_gtk_action(self._on_spoke_clicked, None, None,spoke)
fire_gtk_action(self._on_spoke_clicked, None, None,spoke)
else:log.info("autostep for hub %s finished",self.__class__.__name__)
gtk_call_once(self._doPostAutostep)Same as before some blank lines please.
Yep!
- def _doPostAutostep(self):
if self._spokesToStepIn:# there are still spokes that need to be stepped inreturn# 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")Except the three above everything looks good to me. _______________________________________________ anaconda-patches mailing list anaconda-patches@lists.fedorahosted.org https://lists.fedorahosted.org/mailman/listinfo/anaconda-patches
On Mon, 2015-07-27 at 18:55 +0200, Martin Kolman wrote:
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@redhat.com
pyanaconda/constants.py | 4 + pyanaconda/iutil.py | 27 ++++++ pyanaconda/ui/gui/__init__.py | 179 +++++++++++++++++++++++++++++++---- pyanaconda/ui/gui/hubs/__init__.py | 46 ++++++++- pyanaconda/ui/gui/spokes/__init__.py | 9 ++ pyanaconda/ui/gui/spokes/welcome.py | 5 +- 6 files changed, 250 insertions(+), 20 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..9136406 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 copyreturn- target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
- log.info("saving screenshots taken during the installation to: %s" % target_path)
- try:
# create the screenshots directorymkdirChain(target_path)# copy all screenshotsfor 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..8ffc723 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,12 @@ class GUIObject(common.UIObject): Keybinder.init() Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
self._automaticEntry = Falseself._autostepRunning = Falseself._autostepDone = Falseself._autostepDoneCallback = Noneself._lastAutostepSpoke = False- def _findUIFile(self): path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/") dirs = path.split(":")
@@ -171,28 +177,128 @@ 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" % _screenshotIndexelse: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 controlThis is needed for example for installations with an incomplete kickstart,as we need to differentiate the automatic screenshot pass from the userentering a spoke to manually configure things. We also need to skip applyingchanges 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@property
def lastAutostepSpoke(self):
"""Indicates if the screen is the last spoke to be processed on a hub"""return self._lastAutostepSpoke@lastAutostepSpoke.setter
def lastAutostepSpoke(self, value):
self._lastAutostepSpoke = value
Is there any chance these properties will need to be overridden by subclasses and add some more code to just getting/setting the internal attribute? If not, these should be just public attributes read/set directly.
- def autostep(self):
"""Autostep through this graphical object and throughany graphical objects managed by it (such as through spokes for a hub)"""# report that autostep is running to prevent another from startingself.autostepRunning = True# take a screenshot of the current graphical objectif 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_addself.take_screenshot(self.__class__.__name__)self._doAutostep()# doneself.autostepRunning = Falseself.autostepDone = Trueself._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 codethat 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 customizedautostepping 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 +516,27 @@ class MainWindow(Gtk.Window): """ self._setVisibleChild(spoke)
# autostep through the spoke if requiredif spoke.automaticEntry:# we need to use idle_add here to give GTK time to render the spokegtk_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 tothe hub so that we can proceed to the next one."""# it might be possible that autostep is specified, but autoscreenshot isn'tif 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 hubif spoke.lastAutostepSpoke:self.returnToHub()- def returnToHub(self): """Exit a spoke and return to a hub.""" self._setVisibleChild(self._current_action)
@@ -753,11 +880,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")returnelif not self._currentAction.autostepDone:self._currentAction.autostep()returnif 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() returndiff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index ab68893..bb65329 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -27,7 +27,7 @@ from pyanaconda.product import distributionText
from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup +from pyanaconda.ui.gui.utils import gtk_call_once, fire_gtk_action, escape_markup
import logging log = logging.getLogger("anaconda") @@ -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 automaticallyif spoke.automaticEntry:spoke.automaticEntry = Falsereturn# 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,37 @@ 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__)# created a list of all spokes in reverse alphabetic order, we will pop() from it when
create^
# processing all the spokes so the screenshots will actually be in alphabetic orderself._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 spokesself._autostepSpoke()
As jkonecny already suggested, please add some blank lines in this block.
- def _autostepSpoke(self):
if self._spokesToStepIn:spoke = self._spokesToStepIn.pop()self._spokeAutostepIndex += 1log.debug("stepping to spoke %s (%d/%d)", spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes))spoke.automaticEntry = Truespoke.autostepDoneCallback = lambda x: self._autostepSpoke()# this is the last spoke, tell it to return to hub once the screenshot is takenif self._spokesToStepIn == []:spoke.lastAutostepSpoke = True#fire_gtk_action(self._on_spoke_clicked, None, None, spoke)fire_gtk_action(self._on_spoke_clicked, None, None, spoke)
The commented-out line could definitely be removed. And does this really need to wait for self._on_spoke_clicked() to finish? If not, it should use gtk_call_once() instead of fire_gtk_action().
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 inreturn# 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 screengtk_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()
On Tue, 2015-07-28 at 16:11 +0200, Vratislav Podzimek wrote:
On Mon, 2015-07-27 at 18:55 +0200, Martin Kolman wrote:
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@redhat.com
pyanaconda/constants.py | 4 + pyanaconda/iutil.py | 27 ++++++ pyanaconda/ui/gui/__init__.py | 179 +++++++++++++++++++++++++++++++---- pyanaconda/ui/gui/hubs/__init__.py | 46 ++++++++- pyanaconda/ui/gui/spokes/__init__.py | 9 ++ pyanaconda/ui/gui/spokes/welcome.py | 5 +- 6 files changed, 250 insertions(+), 20 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..9136406 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 copyreturn- target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
- log.info("saving screenshots taken during the installation to:
%s" % target_path)
- try:
# create the screenshots directorymkdirChain(target_path)# copy all screenshotsfor filename in os.listdir(SCREENSHOTS_DIRECTORY):shutil.copy(os.path.join(SCREENSHOTS_DIRECTORY,filename), target_path)
- except OSError:
log.exception("saving screenshots to installed systemfailed") diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 9251949..8ffc723 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,12 @@ class GUIObject(common.UIObject): Keybinder.init() Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
self._automaticEntry = Falseself._autostepRunning = Falseself._autostepDone = Falseself._autostepDoneCallback = Noneself._lastAutostepSpoke = False- def _findUIFile(self): path = os.environ.get("UIPATH",
"./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/") dirs = path.split(":") @@ -171,28 +177,128 @@ 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 screenshotoperation is done _last_screenshot_timestamp = time.time()
- def take_screenshot(self, name=None):
"""Take a screenshot of the whole screen (works even withmultiple displays)
:param name: optional name for the screenshot that will beappended 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 underautomatic control
This is needed for example for installations with anincomplete kickstart,
as we need to differentiate the automatic screenshot passfrom the user
entering a spoke to manually configure things. We alsoneed 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 runningautostep"""
return self._autostepRunning- @autostepRunning.setter
- def autostepRunning(self, value):
self._autostepRunning = value- @property
- def autostepDone(self):
"""Report if autostep for this GUIObject has beenfinished"""
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- @property
- def lastAutostepSpoke(self):
"""Indicates if the screen is the last spoke to beprocessed on a hub"""
return self._lastAutostepSpoke- @lastAutostepSpoke.setter
- def lastAutostepSpoke(self, value):
self._lastAutostepSpoke = valueIs there any chance these properties will need to be overridden by subclasses and add some more code to just getting/setting the internal attribute? If not, these should be just public attributes read/set directly.
I meant it for consistency with all the other properties, but it is indeed unlikely we will need to call some code when this property is read or set. So I'll just turn it to a public atribute.
- def autostep(self):
"""Autostep through this graphical object and throughany graphical objects managed by it (such as throughspokes for a hub)
"""# report that autostep is running to prevent another fromstarting
self.autostepRunning = True# take a screenshot of the current graphical objectif self.data.autostep.autoscreenshot:# as autostep is triggered just before leaving ascreen,
# we can safely take a screenshot of the "parent"object at once
# without using idle_addself.take_screenshot(self.__class__.__name__)self._doAutostep()# doneself.autostepRunning = Falseself.autostepDone = Trueself._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 withcustom code
that brings the GUI from the autostepping mode back to thenormal mode.
This usually means to "click" the continue button or itsequivalent.
"""pass- def _doAutostep(self):
"""To be overridden by the given GUIObject sub-class withcustomized
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 +516,27 @@ class MainWindow(Gtk.Window): """ self._setVisibleChild(spoke)
# autostep through the spoke if requiredif spoke.automaticEntry:# we need to use idle_add here to give GTK time torender 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 hubreturn to
the hub so that we can proceed to the next one."""# it might be possible that autostep is specified, butautoscreenshot 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 hubif spoke.lastAutostepSpoke:self.returnToHub()- def returnToHub(self): """Exit a spoke and return to a hub.""" self._setVisibleChild(self._current_action)
@@ -753,11 +880,29 @@ class GraphicalUserInterface(UserInterface): ### SIGNAL HANDLING METHODS ### def _on_continue_clicked(self, win, user_data=None):
# Autostep needs to be triggered just before switching tothe next screen
# (or before quiting the installation if there are no morescreens) to be consistent
# in both fully automatic kickstart installation and forinstallation with an incomplete
# kickstart. Therefore we basically "hook" the continue-clicked signal, start autostepping
# and ignore any other continue-clicked signals untilautostep is done.
# Once autostep finishes, it emits the appropriatecontinue-clicked signal itself,
# switching to the next screen (if any).if self.data.autostep.seen andself._currentAction.handles_autostep:
if self._currentAction.autostepRunning:log.debug("ignoring the continue-clicked signal -autostep is running")
returnelif not self._currentAction.autostepDone:self._currentAction.autostep()returnif 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 beforekilling Anaconda
# (the kickstart post scripts run to early, so we needto copy the screenshots now)
iutil.save_screenshots() Gtk.main_quit() returndiff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py index ab68893..bb65329 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -27,7 +27,7 @@ from pyanaconda.product import distributionText
from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup +from pyanaconda.ui.gui.utils import gtk_call_once, fire_gtk_action, escape_markup
import logging log = logging.getLogger("anaconda") @@ -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 visitedautomatically
if spoke.automaticEntry:spoke.automaticEntry = Falsereturn# Don't take visitedSinceApplied into account here. Itwill always be # True from the line above. if spoke.changed and (not spoke.skipTo or (spoke.skipTo and spoke.applyOnSkip)): @@ -384,3 +394,37 @@ 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__)
# created a list of all spokes in reverse alphabeticorder, we will pop() from it when
create^
# processing all the spokes so the screenshots willactually 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 theasynchronous nature of GtkStack, so we start by
# autostepping to the first spoke, this will trigger acallback that steps to the next spoke,
# until we run out of unvisited spokesself._autostepSpoke()As jkonecny already suggested, please add some blank lines in this block.
Already done. :-)
- def _autostepSpoke(self):
if self._spokesToStepIn:spoke = self._spokesToStepIn.pop()self._spokeAutostepIndex += 1log.debug("stepping to spoke %s (%d/%d)",spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes))
spoke.automaticEntry = Truespoke.autostepDoneCallback = lambda x:self._autostepSpoke()
# this is the last spoke, tell it to return to hubonce the screenshot is taken
if self._spokesToStepIn == []:spoke.lastAutostepSpoke = True#fire_gtk_action(self._on_spoke_clicked, None, None,spoke)
fire_gtk_action(self._on_spoke_clicked, None, None,spoke)
The commented-out line could definitely be removed. And does this really need to wait for self._on_spoke_clicked() to finish? If not, it should use gtk_call_once() instead of fire_gtk_action().
Done.
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 inreturn# 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 screengtk_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 finalrelease.
if not isFinal:
# Don't display the betanag dialog if this is the finalrelease or
# when autostep has been requested as betanag breaks theautostep 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()
Changes from v1: - more blank lines and comments in complex code - replaced a redundant property with public attribute - replaced fire_gtk_action() by gtk_call_once() for entering spokes - removed an overlooked commented-out line of code
Martin Kolman (1): Add support for autostep and --autoscreenshot (#1234896)
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(-)
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@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()
On Tue, 2015-07-28 at 17:03 +0200, Martin Kolman wrote:
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@redhat.com
Everything seems ok for me now. Ack.
On Tue, 2015-07-28 at 17:03 +0200, Martin Kolman wrote:
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@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 copyreturn- target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
- log.info("saving screenshots taken during the installation to: %s", target_path)
- try:
# create the screenshots directorymkdirChain(target_path)# copy all screenshotsfor 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 = Falseself._autostepRunning = Falseself._autostepDone = Falseself._autostepDoneCallback = None# this indicates if the screen is the last spoke to be processed for a hubself.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" % _screenshotIndexelse: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 controlThis is needed for example for installations with an incomplete kickstart,as we need to differentiate the automatic screenshot pass from the userentering a spoke to manually configure things. We also need to skip applyingchanges 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
I meant my comment about the properties to be replaced by public attributes for all these properties that do nothing in their getters/setters. If you agree that these all could be changed to public attributes, please change them (there's no need to send v3 just because of this change).
Otherwise the new version looks good to me.
anaconda-patches@lists.fedorahosted.org