#
# display.py: graphical display setup for the Anaconda GUI
#
# Copyright (C) 2016
# Red Hat, Inc. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# Author(s): Martin Kolman
#
import os
import time
import textwrap
import pkgutil
import signal
from pyanaconda.mutter_display import MutterDisplay, MutterConfigError
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.path import join_paths
from pyanaconda.core.process_watchers import WatchProcesses
from pyanaconda import startup_utils
from pyanaconda.core import util, constants, hw
from pyanaconda.gnome_remote_destop import GRDServer
from pyanaconda.core.i18n import _
from pyanaconda.flags import flags
from pyanaconda.modules.common.constants.services import NETWORK
from pyanaconda.ui.tui.spokes.askrd import AskRDSpoke, RDPAuthSpoke
from pyanaconda.ui.tui import tui_quit_callback
# needed for checking if the pyanaconda.ui.gui modules are available
import pyanaconda.ui
import blivet
from pykickstart.constants import DISPLAY_MODE_TEXT
from simpleline import App
from simpleline.render.screen_handler import ScreenHandler
from systemd import journal
from pyanaconda.anaconda_loggers import get_module_logger, get_stdout_logger
log = get_module_logger(__name__)
stdout_log = get_stdout_logger()
WAYLAND_TIMEOUT_ADVICE = \
"Do not load the stage2 image over a slow network link.\n" \
"Wait longer for Wayland startup with the inst.xtimeout= boot option." \
"The default is 60 seconds.\n" \
"Load the stage2 image into memory with the rd.live.ram boot option to decrease access " \
"time.\n" \
"Enforce text mode when installing from remote media with the inst.text boot option."
# on RHEL also: "Use the customer portal download URL in ilo/drac devices for greater speed."
def start_user_systemd():
"""Start the user instance of systemd.
The service org.a11y.Bus runs the dbus-broker-launch in
the user scope that requires the user instance of systemd.
"""
if not conf.system.can_start_user_systemd:
log.debug("Don't start the user instance of systemd.")
return
# Start the user instance of systemd. This call will also cause the launch of
# dbus-broker and start a session bus at XDG_RUNTIME_DIR/bus.
childproc = util.startProgram(["/usr/lib/systemd/systemd", "--user"])
WatchProcesses.watch_process(childproc, "systemd")
# Set up the session bus address. Some services started by Anaconda might call
# dbus-launch with the --autolaunch option to find the existing session bus (or
# start a new one), but dbus-launch doesn't check the XDG_RUNTIME_DIR/bus path.
xdg_runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
session_bus_address = "unix:path=" + join_paths(xdg_runtime_dir, "/bus")
os.environ["DBUS_SESSION_BUS_ADDRESS"] = session_bus_address
log.info("The session bus address is set to %s.", session_bus_address)
# Spice
def start_spice_vd_agent():
"""Start the spice vdagent.
For certain features to work spice requires that the guest os
is running the spice vdagent.
"""
try:
status = util.execWithRedirect("spice-vdagent", [])
except OSError as e:
log.warning("spice-vdagent failed: %s", e)
return
if status:
log.info("spice-vdagent exited with status %d", status)
else:
log.info("Started spice-vdagent.")
# RDP
def ask_rd_question(anaconda, grd_server, message):
""" Ask the user if TUI or GUI-over-RDP should be started.
:param anaconda: instance of the Anaconda class
:param grd_server: instance of the GRD server object
:param str message: a message to show to the user together
with the question
:return: if remote desktop should be used
:rtype: bool
"""
App.initialize()
loop = App.get_event_loop()
loop.set_quit_callback(tui_quit_callback)
spoke = AskRDSpoke(anaconda.ksdata, message=message)
ScreenHandler.schedule_screen(spoke)
App.run()
if spoke.use_remote_desktop:
if not anaconda.gui_mode:
log.info("RDP requested via RDP question, switching Anaconda to GUI mode.")
anaconda.display_mode = constants.DisplayModes.GUI
flags.use_rd = True
grd_server.rdp_username = spoke.rdp_username
grd_server.rdp_password = spoke.rdp_password
return spoke.use_remote_desktop
def ask_for_rd_credentials(anaconda, grd_server, username=None, password=None):
""" Ask the user to provide RDP credentials interactively.
:param anaconda: instance of the Anaconda class
:param grd_server: instance of the GRD server object
:param str username: user set username (if any)
:param str password: user set password (if any)
:rtype: bool
"""
App.initialize()
loop = App.get_event_loop()
loop.set_quit_callback(tui_quit_callback)
spoke = RDPAuthSpoke(anaconda.ksdata, username=username, password=password)
ScreenHandler.schedule_screen(spoke)
App.run()
log.info("RDP credentials set")
anaconda.display_mode = constants.DisplayModes.GUI
flags.use_rd = True
grd_server.rdp_username = spoke._username
grd_server.rdp_password = spoke._password
def check_rd_can_be_started(anaconda):
"""Check if we can start an RDP session in the current environment.
:returns: if RDP session can be started and list of possible reasons
why the session can't be started
:rtype: (boot, list)
"""
error_messages = []
rd_startup_possible = True
# disable remote desktop over text question when not enough memory is available
min_gui_ram = hw.minimal_memory_needed(with_gui=True)
if blivet.util.total_memory() < min_gui_ram:
error_messages.append("Not asking for remote desktop session because current memory "
"(%d) < MIN_GUI_RAM (%d)" %
(blivet.util.total_memory(), min_gui_ram))
rd_startup_possible = False
# disable remote desktop question if text mode is requested and this is a ks install
if anaconda.tui_mode and flags.automatedInstall:
error_messages.append(
"Not asking for remote desktop session because of an automated install"
)
rd_startup_possible = False
# disable remote desktop question if we were explicitly asked for text in kickstart
if anaconda.ksdata.displaymode.displayMode == DISPLAY_MODE_TEXT:
error_messages.append("Not asking for remote desktop session because text mode "
"was explicitly asked for in kickstart")
rd_startup_possible = False
# disable remote desktop question if we don't have network
network_proxy = NETWORK.get_proxy()
if not network_proxy.IsConnecting() and not network_proxy.Connected:
error_messages.append("Not asking for RDP mode because we don't have a network")
rd_startup_possible = False
# disable remote desktop question if we don't have GNOME remote desktop
if not os.access('/usr/bin/grdctl', os.X_OK):
error_messages.append("Not asking for remote desktop because we don't have grdctl")
rd_startup_possible = False
return rd_startup_possible, error_messages
def do_startup_wl_actions(timeout, headless=False, headless_resolution=None):
"""Start the Wayland compositor.
Add XDG_DATA_DIRS to the environment to pull in our overridden schema
files.
:param bool headless: start a headless session (used for RDP access)
:param str headless_resolution: headless virtual monitor resolution in WxH format
"""
datadir = os.environ.get('ANACONDA_DATADIR', '/usr/share/anaconda')
if 'XDG_DATA_DIRS' in os.environ:
xdg_data_dirs = datadir + '/window-manager:' + os.environ['XDG_DATA_DIRS']
else:
xdg_data_dirs = datadir + '/window-manager:/usr/share'
xdg_config_dirs = datadir
if 'XDG_CONFIG_DIRS' in os.environ:
xdg_config_dirs = datadir + ':' + os.environ['XDG_CONFIG_DIRS']
os.environ['XDG_CONFIG_DIRS'] = xdg_config_dirs
os.environ["XDG_SESSION_TYPE"] = "wayland"
def wl_preexec():
# to set GUI subprocess SIGINT handler
signal.signal(signal.SIGINT, signal.SIG_IGN)
# lets compile arguments for the run-in-new-session script
argv = ["/usr/libexec/anaconda/run-in-new-session",
"--user", "root",
"--service", "anaconda",
"--session-type", "wayland",
"--session-class", "user"]
if headless:
# headless (remote connection) - stay on VT1 where connection info is
argv.extend(["--vt", "1"])
else:
# local display - switch to VT6 & show GUI there
argv.extend(["--vt", "6"])
# add the generic GNOME Kiosk invocation
argv.extend(["gnome-kiosk", "--sm-disable",
"--wayland", "--no-x11",
"--wayland-display", constants.WAYLAND_SOCKET_NAME])
# remote access needs gnome-kiosk to start in headless mode
if headless:
argv.extend(["--headless"])
# redirect stdout and stderr from GNOME Kiosk to journal
gnome_kiosk_stdout_stream = journal.stream("gnome-kiosk", priority=journal.LOG_INFO)
gnome_kiosk_stderr_stream = journal.stream("gnome-kiosk", priority=journal.LOG_ERR)
childproc = util.startProgram(argv, env_add={'XDG_DATA_DIRS': xdg_data_dirs},
preexec_fn=wl_preexec,
stdout=gnome_kiosk_stdout_stream,
stderr=gnome_kiosk_stderr_stream,
)
WatchProcesses.watch_process(childproc, argv[0])
for _i in range(0, int(timeout / 0.1)):
wl_socket_path = os.path.join(os.getenv("XDG_RUNTIME_DIR"), constants.WAYLAND_SOCKET_NAME)
if os.path.exists(wl_socket_path):
return
time.sleep(0.1)
WatchProcesses.unwatch_process(childproc)
childproc.terminate()
raise TimeoutError("Timeout trying to start gnome-kiosk")
def set_resolution(runres):
"""Set the screen resolution.
:param str runres: a resolution specification string
"""
try:
log.info("Setting the screen resolution to: %s.", runres)
mutter_display = MutterDisplay()
mutter_display.set_resolution(runres)
except MutterConfigError as error:
log.error("The resolution was not set: %s", error)
# general display startup
def setup_display(anaconda, options):
"""Setup the display for the installation environment.
:param anaconda: instance of the Anaconda class
:param options: command line/boot options
"""
anaconda.display_mode = options.display_mode
anaconda.interactive_mode = not options.noninteractive
if flags.rescue_mode:
return
if conf.target.is_image or conf.target.is_directory:
anaconda.log_display_mode()
anaconda.initialize_interface()
return
try:
xtimeout = int(options.xtimeout)
except ValueError:
log.warning("invalid inst.xtimeout option value: %s", options.xtimeout)
xtimeout = constants.X_TIMEOUT
grd_server = GRDServer(anaconda) # The RDP server object
rdp_credentials_sufficient = False
if options.rdp_enabled:
flags.use_rd = True
if not anaconda.gui_mode:
log.info("RDP requested via boot/CLI option, switching Anaconda to GUI mode.")
anaconda.display_mode = constants.DisplayModes.GUI
grd_server.rdp_username = options.rdp_username
grd_server.rdp_password = options.rdp_password
# note if we have both set
rdp_credentials_sufficient = options.rdp_username and options.rdp_password
# check if GUI without WebUI
if anaconda.gui_mode and not anaconda.is_webui_supported:
mods = (tup[1] for tup in pkgutil.iter_modules(pyanaconda.ui.__path__, "pyanaconda.ui."))
if "pyanaconda.ui.gui" not in mods:
stdout_log.warning("Graphical user interface not available, falling back to text mode")
anaconda.display_mode = constants.DisplayModes.TUI
flags.use_rd = False
flags.rd_question = False
# check if remote desktop mode can be started
rd_can_be_started, rd_error_messages = check_rd_can_be_started(anaconda)
if rd_can_be_started:
# if remote desktop can be started & only inst.rdp
# or inst.rdp and insufficient credentials are provided
# via boot options, ask interactively.
if options.rdp_enabled and not rdp_credentials_sufficient:
ask_for_rd_credentials(anaconda, grd_server, options.rdp_username, options.rdp_password)
else:
# RDP can't be started - disable the RDP question and log
# all the errors that prevented RDP from being started
flags.rd_question = False
for error_message in rd_error_messages:
stdout_log.warning(error_message)
if anaconda.tui_mode and flags.rd_question:
# we prefer remote desktop over text mode, so ask about that
message = _("Text mode provides a limited set of installation "
"options. It does not offer custom partitioning for "
"full control over the disk layout. Would you like "
"to use remote graphical access via the RDP protocol instead?")
if not ask_rd_question(anaconda, grd_server, message):
# user has explicitly specified text mode
flags.rd_question = False
anaconda.log_display_mode()
startup_utils.check_memory(anaconda, options)
# check_memory may have changed the display mode
want_gui = anaconda.gui_mode and not (flags.preexisting_wayland or flags.use_rd)
if want_gui:
try:
do_startup_wl_actions(xtimeout)
except TimeoutError as e:
log.warning("Wayland startup failed: %s", e)
print("\nWayland did not start in the expected time, falling back to text mode. "
"There are multiple ways to avoid this issue:")
wrapper = textwrap.TextWrapper(initial_indent=" * ", subsequent_indent=" ",
width=os.get_terminal_size().columns - 3)
for line in WAYLAND_TIMEOUT_ADVICE.split("\n"):
print(wrapper.fill(line))
util.vtActivate(1)
anaconda.display_mode = constants.DisplayModes.TUI
anaconda.gui_startup_failed = True
time.sleep(2)
except (OSError, RuntimeError) as e:
log.warning("Wayland startup failed: %s", e)
print("\nWayland startup failed, falling back to text mode.")
util.vtActivate(1)
anaconda.display_mode = constants.DisplayModes.TUI
anaconda.gui_startup_failed = True
time.sleep(2)
if not anaconda.gui_startup_failed:
if options.runres and anaconda.gui_mode and not flags.use_rd:
def on_mutter_ready(observer):
set_resolution(options.runres)
observer.disconnect()
mutter_display = MutterDisplay()
mutter_display.on_service_ready(on_mutter_ready)
if anaconda.tui_mode and anaconda.gui_startup_failed and flags.rd_question:
message = _("Wayland was unable to start on your machine. Would you like to start "
"an RDP session to connect to this computer from another computer and "
"perform a graphical installation or continue with a text mode "
"installation?")
ask_rd_question(anaconda, grd_server, message)
# if they want us to use RDP do that now
if anaconda.gui_mode and flags.use_rd:
do_startup_wl_actions(xtimeout, headless=True, headless_resolution=options.runres)
grd_server.start_grd_rdp()
# with Wayland running we can initialize the UI interface
anaconda.initialize_interface()
if anaconda.gui_startup_failed:
# we need to reinitialize the locale if GUI startup failed,
# as we might now be in text mode, which might not be able to display
# the characters from our current locale
startup_utils.reinitialize_locale(text_mode=anaconda.tui_mode)