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()