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 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
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.
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 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
create^
# 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()
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 += 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)
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 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()