420 lines
17 KiB
Python
420 lines
17 KiB
Python
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
# Author(s): Martin Kolman <mkolman@redhat.com>
|
|
#
|
|
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=<SECONDS> 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)
|