diff --git a/anaconda.spec.in b/anaconda.spec.in index 3246bd59b66c..458bfd7ecb62 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -248,15 +248,20 @@ Requires: zram-generator # needed for proper driver disk support - if RPMs must be installed, a repo is needed Requires: createrepo_c # Display stuff moved from lorax templates +## For Wayland +Requires: weston +Requires: xorg-x11-server-Xwayland +## For X11 Requires: xorg-x11-drivers Requires: xorg-x11-server-Xorg Requires: xrandr +Requires: gnome-kiosk +## Common stuff Requires: xrdb Requires: dbus-x11 Requires: gsettings-desktop-schemas Requires: nm-connection-editor Requires: librsvg2 -Requires: gnome-kiosk Requires: brltty # dependencies for rpm-ostree payload module Requires: rpm-ostree >= %{rpmostreever} @@ -394,6 +399,7 @@ rm -rf \ %{_sbindir}/anaconda %{_sbindir}/handle-sshpw %{_datadir}/anaconda +%{_sysconfdir}/pam.d/anaconda %{_prefix}/libexec/anaconda %exclude %{_datadir}/anaconda/gnome %exclude %{_datadir}/anaconda/pixmaps diff --git a/data/Makefile.am b/data/Makefile.am index df3974fb7604..5406e9d4f95d 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -SUBDIRS = command-stubs liveinst systemd pixmaps window-manager dbus conf.d profile.d +SUBDIRS = command-stubs liveinst systemd pixmaps window-manager dbus conf.d profile.d pam CLEANFILES = *~ diff --git a/data/pam/Makefile.am b/data/pam/Makefile.am new file mode 100644 index 000000000000..e92103f0bc05 --- /dev/null +++ b/data/pam/Makefile.am @@ -0,0 +1,21 @@ +# Copyright (C) 2023 Neal Gompa. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +CLEANFILES = *~ + +pamdir = $(sysconfdir)/pam.d +dist_pam_DATA = anaconda + +MAINTAINERCLEANFILES = Makefile.in diff --git a/data/pam/anaconda b/data/pam/anaconda new file mode 100644 index 000000000000..4e6d91adf2c5 --- /dev/null +++ b/data/pam/anaconda @@ -0,0 +1,8 @@ +#%PAM-1.0 +auth sufficient pam_permit.so +account sufficient pam_permit.so +password sufficient pam_permit.so +session required pam_loginuid.so +-session optional pam_keyinit.so revoke +-session optional pam_limits.so +session required pam_systemd.so diff --git a/data/systemd/anaconda.service b/data/systemd/anaconda.service index a80c6bb7075c..23918415c388 100644 --- a/data/systemd/anaconda.service +++ b/data/systemd/anaconda.service @@ -8,3 +8,4 @@ Type=forking Environment=HOME=/root MALLOC_CHECK_=2 MALLOC_PERTURB_=204 PATH=/usr/bin:/bin:/sbin:/usr/sbin:/mnt/sysimage/bin:/mnt/sysimage/usr/bin:/mnt/sysimage/usr/sbin:/mnt/sysimage/sbin LANG=en_US.UTF-8 GDK_BACKEND=x11 XDG_RUNTIME_DIR=/tmp GIO_USE_VFS=local WorkingDirectory=/root ExecStart=/usr/bin/tmux -u -f /usr/share/anaconda/tmux.conf start +PAMName=anaconda diff --git a/pyanaconda/core/constants.py b/pyanaconda/core/constants.py index 40e9adf171ba..17881ea28631 100644 --- a/pyanaconda/core/constants.py +++ b/pyanaconda/core/constants.py @@ -261,6 +261,9 @@ class SecretStatus(Enum): IPMI_ABORTED = 0x9 # installation finished unsuccessfully, due to some non-exn error IPMI_FAILED = 0xA # installation hit an exception +# Wayland socket name to use +WAYLAND_SOCKET_NAME = "wl-sysinstall-0" + # X display number to use X_DISPLAY_NUMBER = 1 @@ -315,6 +318,14 @@ class DisplayModes(Enum): False: "noninteractive" } +# Weston configuration +WESTON_CONFIG = { + "core": { + "shell": "kiosk", + "xwayland": "true" + } +} + # Loggers LOGGER_ANACONDA_ROOT = "anaconda" LOGGER_MAIN = "anaconda.main" @@ -322,6 +333,9 @@ class DisplayModes(Enum): LOGGER_PROGRAM = "program" LOGGER_SIMPLELINE = "simpleline" +# Timeout for starting Wayland +WAYLAND_TIMEOUT = 60 + # Timeout for starting X X_TIMEOUT = 60 diff --git a/pyanaconda/core/util.py b/pyanaconda/core/util.py index 8005a52eb3c4..446d837c0370 100644 --- a/pyanaconda/core/util.py +++ b/pyanaconda/core/util.py @@ -18,6 +18,7 @@ # along with this program. If not, see . # +import configparser import os import os.path import subprocess @@ -40,7 +41,8 @@ from pyanaconda.core.path import make_directories, open_with_perm, join_paths from pyanaconda.core.process_watchers import WatchProcesses from pyanaconda.core.constants import DRACUT_SHUTDOWN_EJECT, \ - IPMI_ABORTED, X_TIMEOUT, PACKAGES_LIST_FILE + IPMI_ABORTED, WAYLAND_SOCKET_NAME, WAYLAND_TIMEOUT, \ + WESTON_CONFIG, X_TIMEOUT, PACKAGES_LIST_FILE from pyanaconda.core.live_user import get_live_user from pyanaconda.errors import RemovedModuleError @@ -165,6 +167,68 @@ def preexec(): return partsubp(preexec_fn=preexec) +class WaylandStatus: + """Status of Wayland launch. + + Values of an instance can be modified from the handler functions. + """ + def __init__(self): + self.started = False + self.timed_out = False + + def needs_waiting(self): + return not (self.started or self.timed_out) + + +def startWl(weston_config=WESTON_CONFIG, output_redirect=None, timeout=WAYLAND_TIMEOUT): + """ Start Weston for Wayland and return once Weston is ready to accept connections. + + We can identify whether Weston is ready by testing if + the Wayland socket is open yet. Once it is, we can return success. + + :param weston_config: The weston.ini(5) configuration to use, as a dictionary + :param output_redirect: file or file descriptor to redirect stdout and stderr to + :param timeout: Number of seconds to timing out. + """ + wl_status = WaylandStatus() + + # Create the config file for Weston + weston_config_file = tempfile.NamedTemporaryFile(suffix="-wl-weston-sysinstall-ini", + delete=False) + weston_config_ini = configparser.ConfigParser() + for section, options in weston_config.items(): + weston_config_ini.add_section(section) + for key, value in options.items(): + weston_config_ini.set(section, key, str(value)) + weston_config_ini.write(weston_config_file) + + log.debug("Starting Weston.") + argv = ["weston", f"--config={weston_config_file}", "--log=/tmp/weston.log", + f"--socket={WAYLAND_SOCKET_NAME}"] + + childproc = startProgram(argv, stdout=output_redirect, stderr=output_redirect) + WatchProcesses.watch_process(childproc, argv[0]) + + for _ in range(0, timeout): + try: + xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") + path = os.path.join(xdg_runtime_dir, WAYLAND_SOCKET_NAME) + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) + sock.connect(path) + sock.close() + wl_status.started = True + return wl_status.started + except Exception: + if wl_status.needs_waiting(): + sleep(1) + wl_status.timed_out = True + WatchProcesses.unwatch_process(childproc) + childproc.terminate() + log.debug("Exception handler test suspended to prevent accidental activation by " + "delayed Weston start.") + raise TimeoutError("Timeout trying to start %s" % argv[0]) + + class X11Status: """Status of Xorg launch. diff --git a/pyanaconda/display.py b/pyanaconda/display.py index 8e5527161e21..457056973a45 100644 --- a/pyanaconda/display.py +++ b/pyanaconda/display.py @@ -1,8 +1,7 @@ # # display.py: graphical display setup for the Anaconda GUI # -# Copyright (C) 2016 -# Red Hat, Inc. All rights reserved. +# Copyright (C) 2024 Neal Gompa. 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 @@ -17,233 +16,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# Author(s): Martin Kolman +# Author(s): Neal Gompa # -import os -import subprocess -import time -import textwrap -import pkgutil -import signal -from pyanaconda.core.configuration.anaconda import conf -from pyanaconda.core.process_watchers import WatchProcesses -from pyanaconda import startup_utils -from pyanaconda.core import util, constants, hw -from pyanaconda import vnc -from pyanaconda.core.i18n import _ -from pyanaconda.flags import flags -from pyanaconda.modules.common.constants.services import NETWORK -from pyanaconda.ui.tui.spokes.askvnc import AskVNCSpoke -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 pyanaconda.anaconda_loggers import get_module_logger, get_stdout_logger log = get_module_logger(__name__) stdout_log = get_stdout_logger() -X_TIMEOUT_ADVICE = \ - "Do not load the stage2 image over a slow network link.\n" \ - "Wait longer for the X server 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 - - childproc = util.startProgram(["/usr/lib/systemd/systemd", "--user"]) - WatchProcesses.watch_process(childproc, "systemd") - - -# 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.") - - -# VNC - -def ask_vnc_question(anaconda, vnc_server, message): - """ Ask the user if TUI or GUI-over-VNC should be started. - - :param anaconda: instance of the Anaconda class - :param vnc_server: instance of the VNC server object - :param str message: a message to show to the user together - with the question - """ - App.initialize() - loop = App.get_event_loop() - loop.set_quit_callback(tui_quit_callback) - spoke = AskVNCSpoke(anaconda.ksdata, message=message) - ScreenHandler.schedule_screen(spoke) - App.run() - - if anaconda.ksdata.vnc.enabled: - if not anaconda.gui_mode: - log.info("VNC requested via VNC question, switching Anaconda to GUI mode.") - anaconda.display_mode = constants.DisplayModes.GUI - flags.usevnc = True - vnc_server.password = anaconda.ksdata.vnc.password - - -def check_vnc_can_be_started(anaconda): - """Check if we can start VNC in the current environment. - - :returns: if VNC can be started and list of possible reasons - why VNC can't be started - :rtype: (boot, list) - """ - - error_messages = [] - vnc_startup_possible = True - - # disable VNC 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 VNC because current memory (%d) < MIN_GUI_RAM (%d)" % - (blivet.util.total_memory(), min_gui_ram)) - vnc_startup_possible = False - - # disable VNC 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 VNC because of an automated install") - vnc_startup_possible = False - - # disable VNC question if we were explicitly asked for text in kickstart - if anaconda.ksdata.displaymode.displayMode == DISPLAY_MODE_TEXT: - error_messages.append("Not asking for VNC because text mode was explicitly asked for in kickstart") - vnc_startup_possible = False - - # disable VNC 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 VNC because we don't have a network") - vnc_startup_possible = False - - # disable VNC question if we don't have Xvnc - if not os.access('/usr/bin/Xvnc', os.X_OK): - error_messages.append("Not asking for VNC because we don't have Xvnc") - vnc_startup_possible = False - - return vnc_startup_possible, error_messages - - -# X11 - -def start_x11(xtimeout): - """Start the X server for the Anaconda GUI.""" - - # Start Xorg and wait for it become ready - util.startX(["Xorg", "-br", "-logfile", "/tmp/X.log", - ":%s" % constants.X_DISPLAY_NUMBER, "vt6", "-s", "1440", "-ac", - "-nolisten", "tcp", "-dpi", "96", - "-noreset"], - output_redirect=subprocess.DEVNULL, timeout=xtimeout) - - -# function to handle X startup special issues for anaconda - -def do_startup_x11_actions(): - """Start the window manager. - - When window manager actually connects to the X server is unknowable, but - fortunately it doesn't matter. Wm does not need to be the first - connection to Xorg, and if anaconda starts up before wm, wm - will just take over and maximize the window and make everything right, - fingers crossed. - Add XDG_DATA_DIRS to the environment to pull in our overridden schema - files. - """ - 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' - - def x11_preexec(): - # to set GUI subprocess SIGINT handler - signal.signal(signal.SIGINT, signal.SIG_IGN) - - childproc = util.startProgram(["gnome-kiosk", "--display", ":1", "--sm-disable", "--x11"], - env_add={'XDG_DATA_DIRS': xdg_data_dirs}, - preexec_fn=x11_preexec) - WatchProcesses.watch_process(childproc, "gnome-kiosk") - - -def set_x_resolution(runres): - """Set X server screen resolution. - - :param str runres: a resolution specification string - """ - try: - log.info("Setting the screen resolution to: %s.", runres) - util.execWithRedirect("xrandr", ["-d", ":1", "-s", runres]) - except RuntimeError: - log.error("The X resolution was not set") - util.execWithRedirect("xrandr", ["-d", ":1", "-q"]) - - -def do_extra_x11_actions(runres, gui_mode): - """Perform X11 actions not related to startup. - - :param str runres: a resolution specification string - :param gui_mode: an Anaconda display mode - """ - if runres and gui_mode and not flags.usevnc: - set_x_resolution(runres) - - # Load the system-wide Xresources - util.execWithRedirect("xrdb", ["-nocpp", "-merge", "/etc/X11/Xresources"]) - - start_user_systemd() - start_spice_vd_agent() - - -def write_xdriver(driver, root=None): - """Write the X driver.""" - if root is None: - root = conf.target.system_root - - if not os.path.isdir("%s/etc/X11" % (root,)): - os.makedirs("%s/etc/X11" % (root,), mode=0o755) - - f = open("%s/etc/X11/xorg.conf" % (root,), 'w') - f.write('Section "Device"\n\tIdentifier "Videocard0"\n\tDriver "%s"\nEndSection\n' % driver) - f.close() - # general display startup def setup_display(anaconda, options): @@ -253,138 +33,9 @@ def setup_display(anaconda, options): :param options: command line/boot options """ - try: - xtimeout = int(options.xtimeout) - except ValueError: - log.warning("invalid inst.xtimeout option value: %s", options.xtimeout) - xtimeout = constants.X_TIMEOUT - - vnc_server = vnc.VncServer() # The vnc Server object. - vnc_server.anaconda = anaconda - vnc_server.timeout = xtimeout - - anaconda.display_mode = options.display_mode - anaconda.interactive_mode = not options.noninteractive - - if options.vnc: - flags.usevnc = True - if not anaconda.gui_mode: - log.info("VNC requested via boot/CLI option, switching Anaconda to GUI mode.") - anaconda.display_mode = constants.DisplayModes.GUI - vnc_server.password = options.vncpassword - - # Only consider vncconnect when vnc is a param - if options.vncconnect: - cargs = options.vncconnect.split(":") - vnc_server.vncconnecthost = cargs[0] - if len(cargs) > 1 and len(cargs[1]) > 0: - if len(cargs[1]) > 0: - vnc_server.vncconnectport = cargs[1] - - if options.xdriver: - write_xdriver(options.xdriver, root="/") - - if flags.rescue_mode: - return - - if anaconda.ksdata.vnc.enabled: - flags.usevnc = True - if not anaconda.gui_mode: - log.info("VNC requested via kickstart, switching Anaconda to GUI mode.") - anaconda.display_mode = constants.DisplayModes.GUI - - if vnc_server.password == "": - vnc_server.password = anaconda.ksdata.vnc.password - - if vnc_server.vncconnecthost == "": - vnc_server.vncconnecthost = anaconda.ksdata.vnc.host - - if vnc_server.vncconnectport == "": - vnc_server.vncconnectport = anaconda.ksdata.vnc.port - - # 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.usevnc = False - flags.vncquestion = False - - # check if VNC can be started - vnc_can_be_started, vnc_error_messages = check_vnc_can_be_started(anaconda) - if not vnc_can_be_started: - # VNC can't be started - disable the VNC question and log - # all the errors that prevented VNC from being started - flags.vncquestion = False - for error_message in vnc_error_messages: - stdout_log.warning(error_message) - - # Should we try to start Xorg? - want_x = anaconda.gui_mode and not (flags.preexisting_x11 or flags.usevnc) - - # Is Xorg is actually available? - if want_x and not os.access("/usr/bin/Xorg", os.X_OK): - stdout_log.warning(_("Graphical installation is not available. " - "Starting text mode.")) - time.sleep(2) - anaconda.display_mode = constants.DisplayModes.TUI - want_x = False - - if anaconda.tui_mode and flags.vncquestion: - # we prefer vnc 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 VNC mode instead?") - ask_vnc_question(anaconda, vnc_server, message) - if not anaconda.ksdata.vnc.enabled: - # user has explicitly specified text mode - flags.vncquestion = False - - anaconda.log_display_mode() - startup_utils.check_memory(anaconda, options) - - # check_memory may have changed the display mode - want_x = want_x and (anaconda.gui_mode) - if want_x: - try: - start_x11(xtimeout) - do_startup_x11_actions() - except TimeoutError as e: - log.warning("X startup failed: %s", e) - print("\nX 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 X_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("X or window manager startup failed: %s", e) - print("\nX or window manager 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: - do_extra_x11_actions(options.runres, gui_mode=anaconda.gui_mode) - - if anaconda.tui_mode and anaconda.gui_startup_failed and flags.vncquestion and not anaconda.ksdata.vnc.enabled: - message = _("X was unable to start on your machine. Would you like to start VNC to connect to " - "this computer from another computer and perform a graphical installation or continue " - "with a text mode installation?") - ask_vnc_question(anaconda, vnc_server, message) - - # if they want us to use VNC do that now - if anaconda.gui_mode and flags.usevnc: - vnc_server.startServer() - do_startup_x11_actions() - - # with X running we can initialize the UI interface - anaconda.initInterface() + if options.x11: + from pyanaconda import display_x11 + display_x11.setup_display(anaconda, opts) + else: + from pyanaconda import display_wayland + display_wayland.setup_display(anaconda, opts) diff --git a/pyanaconda/display_wayland.py b/pyanaconda/display_wayland.py new file mode 100644 index 000000000000..ff4de9756ec9 --- /dev/null +++ b/pyanaconda/display_wayland.py @@ -0,0 +1,333 @@ +# +# display_wayland.py: Wayland graphical display setup for the Anaconda GUI +# +# Copyright (C) 2024 Neal Gompa. +# +# 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): Neal Gompa +# +import glob +import os +import subprocess +import time +import textwrap +import pkgutil +import signal + +from pyanaconda.core.configuration.anaconda import conf +from pyanaconda.core.process_watchers import WatchProcesses +from pyanaconda import startup_utils +from pyanaconda.core import util, constants, hw +from pyanaconda.core.i18n import _ +from pyanaconda.flags import flags +from pyanaconda.modules.common.constants.services import NETWORK +from pyanaconda.ui.tui.spokes.askvnc import AskVNCSpoke +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 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 the compositor 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 + + childproc = util.startProgram(["/usr/lib/systemd/systemd", "--user"]) + WatchProcesses.watch_process(childproc, "systemd") + + +# 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.") + + +# VNC + +def ask_vnc_question(anaconda, message): + """ Ask the user if TUI or GUI-over-VNC should be started. + + :param anaconda: instance of the Anaconda class + :param vnc_server: instance of the VNC server object + :param str message: a message to show to the user together + with the question + """ + App.initialize() + loop = App.get_event_loop() + loop.set_quit_callback(tui_quit_callback) + spoke = AskVNCSpoke(anaconda.ksdata, message=message) + ScreenHandler.schedule_screen(spoke) + App.run() + + if anaconda.ksdata.vnc.enabled: + if not anaconda.gui_mode: + log.info("VNC requested via VNC question, switching Anaconda to GUI mode.") + anaconda.display_mode = constants.DisplayModes.GUI + flags.usevnc = True + + +def check_vnc_can_be_started(anaconda): + """Check if we can start VNC in the current environment. + + :returns: if VNC can be started and list of possible reasons + why VNC can't be started + :rtype: (boot, list) + """ + + error_messages = [] + vnc_startup_possible = True + + # disable VNC 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 VNC because current memory (%d) < MIN_GUI_RAM (%d)" % + (blivet.util.total_memory(), min_gui_ram)) + vnc_startup_possible = False + + # disable VNC 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 VNC because of an automated install") + vnc_startup_possible = False + + # disable VNC question if we were explicitly asked for text in kickstart + if anaconda.ksdata.displaymode.displayMode == DISPLAY_MODE_TEXT: + error_messages.append("Not asking for VNC because text mode was explicitly asked for in kickstart") + vnc_startup_possible = False + + # disable VNC 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 VNC because we don't have a network") + vnc_startup_possible = False + + # disable VNC question if we don't have Weston's VNC backend + if not glob.glob("/usr/lib*/libweston*/vnc-backend.so"): + error_messages.append("Not asking for VNC because we don't have weston-vnc") + vnc_startup_possible = False + + return vnc_startup_possible, error_messages + +# Wayland + +def start_weston(wconfig, wltimeout): + """Start Weston for the Anaconda GUI""" + + # Declare Weston configuration + # Start Weston and wait for it to become ready + util.vtActivate(6) + util.startWl(weston_config=wconfig, + output_redirect=subprocess.DEVNULL, timeout=wltimeout) + +# X11 + +def start_x11(xtimeout): + """Start the X server for the Anaconda GUI.""" + + # Start Xorg and wait for it become ready + util.startX(["Xwayland", "-rootless", "-logfile", "/tmp/X.log", + ":%s" % constants.X_DISPLAY_NUMBER, "-s", "1440", "-ac", + "-nolisten", "tcp", "-dpi", "96", + "-noreset"], + output_redirect=subprocess.DEVNULL, timeout=xtimeout) + + +def do_extra_x11_actions(): + """Perform X11 actions not related to startup.""" + + # Load the system-wide Xresources + util.execWithRedirect("xrdb", ["-nocpp", "-merge", "/etc/X11/Xresources"]) + + start_user_systemd() + start_spice_vd_agent() + + +# 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 + """ + + try: + wltimeout = int(options.xtimeout) + except ValueError: + log.warning("invalid inst.xtimeout option value: %s", options.xtimeout) + wltimeout = constants.WAYLAND_TIMEOUT + + weston_core_config = { + "core": { + "shell": "kiosk", + } + } + + weston_vnc_config = { + "vnc": { + "port": "5900", + }, + "output": { + "name": "vnc", + "resizeable": "true", + "mode": "800x600", + }, + } + + anaconda.display_mode = options.display_mode + anaconda.interactive_mode = not options.noninteractive + + if options.vnc: + flags.usevnc = True + if not anaconda.gui_mode: + log.info("VNC requested via boot/CLI option, switching Anaconda to GUI mode.") + anaconda.display_mode = constants.DisplayModes.GUI + + if flags.rescue_mode: + return + + if anaconda.ksdata.vnc.enabled: + flags.usevnc = True + if not anaconda.gui_mode: + log.info("VNC requested via kickstart, switching Anaconda to GUI mode.") + anaconda.display_mode = constants.DisplayModes.GUI + + # 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.usevnc = False + flags.vncquestion = False + + # check if VNC can be started + vnc_can_be_started, vnc_error_messages = check_vnc_can_be_started(anaconda) + if not vnc_can_be_started: + # VNC can't be started - disable the VNC question and log + # all the errors that prevented VNC from being started + flags.vncquestion = False + for error_message in vnc_error_messages: + stdout_log.warning(error_message) + + # Are Weston and Xwayland actually available? + have_gui = os.access("/usr/bin/weston", os.X_OK) and os.access("/usr/bin/Xwayland", os.X_OK) + # Should we try to start the graphical environment? + want_gui = anaconda.gui_mode and not (flags.preexisting_x11 or flags.usevnc) + + if want_gui and not have_gui: + stdout_log.warning(_("Graphical installation is not available. " + "Starting text mode.")) + time.sleep(2) + anaconda.display_mode = constants.DisplayModes.TUI + want_gui = False + + if anaconda.tui_mode and have_gui and flags.vncquestion: + # we prefer vnc 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 VNC mode instead?") + ask_vnc_question(anaconda, vnc_server, message) + if not anaconda.ksdata.vnc.enabled: + # user has explicitly specified text mode + flags.vncquestion = False + + anaconda.log_display_mode() + startup_utils.check_memory(anaconda, options) + + # check_memory may have changed the display mode + want_gui = want_gui and (anaconda.gui_mode) + if want_gui: + try: + if flags.usevnc: + start_weston(weston_core_config | weston_vnc_config, wltimeout) + else: + start_weston(weston_core_config, wltimeout) + start_x11(wltimeout) + except TimeoutError as e: + log.warning("Graphics startup failed: %s", e) + print("\nGraphics 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("Xwayland startup failed: %s", e) + print("\nXwayland 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: + do_extra_x11_actions() + + if anaconda.tui_mode and anaconda.gui_startup_failed and flags.vncquestion and not anaconda.ksdata.vnc.enabled: + message = _("Graphics was unable to start on your machine. Would you like to start VNC to connect to " + "this computer from another computer and perform a graphical installation or continue " + "with a text mode installation?") + ask_vnc_question(anaconda, message) + + # if they want us to use VNC do that now + if anaconda.gui_mode and flags.usevnc: + start_weston(weston_core_config | weston_vnc_config, wltimeout) + start_x11(wltimeout) + + # with X running we can initialize the UI interface + anaconda.initInterface() diff --git a/pyanaconda/display_x11.py b/pyanaconda/display_x11.py new file mode 100644 index 000000000000..d46b154915e4 --- /dev/null +++ b/pyanaconda/display_x11.py @@ -0,0 +1,390 @@ +# +# display_x11.py: X11 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 subprocess +import time +import textwrap +import pkgutil +import signal + +from pyanaconda.core.configuration.anaconda import conf +from pyanaconda.core.process_watchers import WatchProcesses +from pyanaconda import startup_utils +from pyanaconda.core import util, constants, hw +from pyanaconda import vnc +from pyanaconda.core.i18n import _ +from pyanaconda.flags import flags +from pyanaconda.modules.common.constants.services import NETWORK +from pyanaconda.ui.tui.spokes.askvnc import AskVNCSpoke +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 pyanaconda.anaconda_loggers import get_module_logger, get_stdout_logger +log = get_module_logger(__name__) +stdout_log = get_stdout_logger() + +X_TIMEOUT_ADVICE = \ + "Do not load the stage2 image over a slow network link.\n" \ + "Wait longer for the X server 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 + + childproc = util.startProgram(["/usr/lib/systemd/systemd", "--user"]) + WatchProcesses.watch_process(childproc, "systemd") + + +# 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.") + + +# VNC + +def ask_vnc_question(anaconda, vnc_server, message): + """ Ask the user if TUI or GUI-over-VNC should be started. + + :param anaconda: instance of the Anaconda class + :param vnc_server: instance of the VNC server object + :param str message: a message to show to the user together + with the question + """ + App.initialize() + loop = App.get_event_loop() + loop.set_quit_callback(tui_quit_callback) + spoke = AskVNCSpoke(anaconda.ksdata, message=message) + ScreenHandler.schedule_screen(spoke) + App.run() + + if anaconda.ksdata.vnc.enabled: + if not anaconda.gui_mode: + log.info("VNC requested via VNC question, switching Anaconda to GUI mode.") + anaconda.display_mode = constants.DisplayModes.GUI + flags.usevnc = True + vnc_server.password = anaconda.ksdata.vnc.password + + +def check_vnc_can_be_started(anaconda): + """Check if we can start VNC in the current environment. + + :returns: if VNC can be started and list of possible reasons + why VNC can't be started + :rtype: (boot, list) + """ + + error_messages = [] + vnc_startup_possible = True + + # disable VNC 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 VNC because current memory (%d) < MIN_GUI_RAM (%d)" % + (blivet.util.total_memory(), min_gui_ram)) + vnc_startup_possible = False + + # disable VNC 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 VNC because of an automated install") + vnc_startup_possible = False + + # disable VNC question if we were explicitly asked for text in kickstart + if anaconda.ksdata.displaymode.displayMode == DISPLAY_MODE_TEXT: + error_messages.append("Not asking for VNC because text mode was explicitly asked for in kickstart") + vnc_startup_possible = False + + # disable VNC 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 VNC because we don't have a network") + vnc_startup_possible = False + + # disable VNC question if we don't have Xvnc + if not os.access('/usr/bin/Xvnc', os.X_OK): + error_messages.append("Not asking for VNC because we don't have Xvnc") + vnc_startup_possible = False + + return vnc_startup_possible, error_messages + + +# X11 + +def start_x11(xtimeout): + """Start the X server for the Anaconda GUI.""" + + # Start Xorg and wait for it become ready + util.startX(["Xorg", "-br", "-logfile", "/tmp/X.log", + ":%s" % constants.X_DISPLAY_NUMBER, "vt6", "-s", "1440", "-ac", + "-nolisten", "tcp", "-dpi", "96", + "-noreset"], + output_redirect=subprocess.DEVNULL, timeout=xtimeout) + + +# function to handle X startup special issues for anaconda + +def do_startup_x11_actions(): + """Start the window manager. + + When window manager actually connects to the X server is unknowable, but + fortunately it doesn't matter. Wm does not need to be the first + connection to Xorg, and if anaconda starts up before wm, wm + will just take over and maximize the window and make everything right, + fingers crossed. + Add XDG_DATA_DIRS to the environment to pull in our overridden schema + files. + """ + 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' + + def x11_preexec(): + # to set GUI subprocess SIGINT handler + signal.signal(signal.SIGINT, signal.SIG_IGN) + + childproc = util.startProgram(["gnome-kiosk", "--display", ":1", "--sm-disable", "--x11"], + env_add={'XDG_DATA_DIRS': xdg_data_dirs}, + preexec_fn=x11_preexec) + WatchProcesses.watch_process(childproc, "gnome-kiosk") + + +def set_x_resolution(runres): + """Set X server screen resolution. + + :param str runres: a resolution specification string + """ + try: + log.info("Setting the screen resolution to: %s.", runres) + util.execWithRedirect("xrandr", ["-d", ":1", "-s", runres]) + except RuntimeError: + log.error("The X resolution was not set") + util.execWithRedirect("xrandr", ["-d", ":1", "-q"]) + + +def do_extra_x11_actions(runres, gui_mode): + """Perform X11 actions not related to startup. + + :param str runres: a resolution specification string + :param gui_mode: an Anaconda display mode + """ + if runres and gui_mode and not flags.usevnc: + set_x_resolution(runres) + + # Load the system-wide Xresources + util.execWithRedirect("xrdb", ["-nocpp", "-merge", "/etc/X11/Xresources"]) + + start_user_systemd() + start_spice_vd_agent() + + +def write_xdriver(driver, root=None): + """Write the X driver.""" + if root is None: + root = conf.target.system_root + + if not os.path.isdir("%s/etc/X11" % (root,)): + os.makedirs("%s/etc/X11" % (root,), mode=0o755) + + f = open("%s/etc/X11/xorg.conf" % (root,), 'w') + f.write('Section "Device"\n\tIdentifier "Videocard0"\n\tDriver "%s"\nEndSection\n' % driver) + f.close() + + +# 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 + """ + + try: + xtimeout = int(options.xtimeout) + except ValueError: + log.warning("invalid inst.xtimeout option value: %s", options.xtimeout) + xtimeout = constants.X_TIMEOUT + + vnc_server = vnc.VncServer() # The vnc Server object. + vnc_server.anaconda = anaconda + vnc_server.timeout = xtimeout + + anaconda.display_mode = options.display_mode + anaconda.interactive_mode = not options.noninteractive + + if options.vnc: + flags.usevnc = True + if not anaconda.gui_mode: + log.info("VNC requested via boot/CLI option, switching Anaconda to GUI mode.") + anaconda.display_mode = constants.DisplayModes.GUI + vnc_server.password = options.vncpassword + + # Only consider vncconnect when vnc is a param + if options.vncconnect: + cargs = options.vncconnect.split(":") + vnc_server.vncconnecthost = cargs[0] + if len(cargs) > 1 and len(cargs[1]) > 0: + if len(cargs[1]) > 0: + vnc_server.vncconnectport = cargs[1] + + if options.xdriver: + write_xdriver(options.xdriver, root="/") + + if flags.rescue_mode: + return + + if anaconda.ksdata.vnc.enabled: + flags.usevnc = True + if not anaconda.gui_mode: + log.info("VNC requested via kickstart, switching Anaconda to GUI mode.") + anaconda.display_mode = constants.DisplayModes.GUI + + if vnc_server.password == "": + vnc_server.password = anaconda.ksdata.vnc.password + + if vnc_server.vncconnecthost == "": + vnc_server.vncconnecthost = anaconda.ksdata.vnc.host + + if vnc_server.vncconnectport == "": + vnc_server.vncconnectport = anaconda.ksdata.vnc.port + + # 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.usevnc = False + flags.vncquestion = False + + # check if VNC can be started + vnc_can_be_started, vnc_error_messages = check_vnc_can_be_started(anaconda) + if not vnc_can_be_started: + # VNC can't be started - disable the VNC question and log + # all the errors that prevented VNC from being started + flags.vncquestion = False + for error_message in vnc_error_messages: + stdout_log.warning(error_message) + + # Should we try to start Xorg? + want_x = anaconda.gui_mode and not (flags.preexisting_x11 or flags.usevnc) + + # Is Xorg is actually available? + if want_x and not os.access("/usr/bin/Xorg", os.X_OK): + stdout_log.warning(_("Graphical installation is not available. " + "Starting text mode.")) + time.sleep(2) + anaconda.display_mode = constants.DisplayModes.TUI + want_x = False + + if anaconda.tui_mode and flags.vncquestion: + # we prefer vnc 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 VNC mode instead?") + ask_vnc_question(anaconda, vnc_server, message) + if not anaconda.ksdata.vnc.enabled: + # user has explicitly specified text mode + flags.vncquestion = False + + anaconda.log_display_mode() + startup_utils.check_memory(anaconda, options) + + # check_memory may have changed the display mode + want_x = want_x and (anaconda.gui_mode) + if want_x: + try: + start_x11(xtimeout) + do_startup_x11_actions() + except TimeoutError as e: + log.warning("X startup failed: %s", e) + print("\nX 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 X_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("X or window manager startup failed: %s", e) + print("\nX or window manager 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: + do_extra_x11_actions(options.runres, gui_mode=anaconda.gui_mode) + + if anaconda.tui_mode and anaconda.gui_startup_failed and flags.vncquestion and not anaconda.ksdata.vnc.enabled: + message = _("X was unable to start on your machine. Would you like to start VNC to connect to " + "this computer from another computer and perform a graphical installation or continue " + "with a text mode installation?") + ask_vnc_question(anaconda, vnc_server, message) + + # if they want us to use VNC do that now + if anaconda.gui_mode and flags.usevnc: + vnc_server.startServer() + do_startup_x11_actions() + + # with X running we can initialize the UI interface + anaconda.initInterface() diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py index 520c9b942e9a..b84f6b9730dd 100644 --- a/pyanaconda/ui/gui/__init__.py +++ b/pyanaconda/ui/gui/__init__.py @@ -672,6 +672,9 @@ def _instantiateAction(self, actionClass): return obj def run(self): + # Ensure we launch as an "X11" application since we do not support anything else yet + util.setenv("GDK_BACKEND", "x11") + (success, _args) = Gtk.init_check(None) if not success: raise RuntimeError("Failed to initialize Gtk")