diff --git a/anaconda.py b/anaconda.py index af304eac889..c0513d77933 100755 --- a/anaconda.py +++ b/anaconda.py @@ -47,7 +47,7 @@ def exitHandler(rebootData): WatchProcesses.unwatch_all_processes() if flags.use_rd: - gnome_remote_destop.shutdown_server() + gnome_remote_desktop.shutdown_server() if "nokill" in kernel_arguments: util.vtActivate(1) @@ -284,7 +284,7 @@ def setup_environment(): opts.display_mode = constants.DisplayModes.TUI opts.noninteractive = True - from pyanaconda import gnome_remote_destop + from pyanaconda import gnome_remote_desktop from pyanaconda import kickstart # we are past the --version and --help shortcut so we can import display & # startup_utils, which import Blivet, without slowing down anything critical diff --git a/data/liveinst/liveinst b/data/liveinst/liveinst index dadcd0f573f..70554b6b408 100755 --- a/data/liveinst/liveinst +++ b/data/liveinst/liveinst @@ -113,6 +113,17 @@ for opt in $(cat /proc/cmdline) "$@"; do fi exit 1 ;; + rdp|rdp.username|rdp.password|--rdp|--rdp.username|--rdp.password) + title="Configuration not supported" + text="RDP is not supported on live media." + if which zenity &> /dev/null; then + zenity --warning --title="$title" --text="$text" + else + echo "$title" >&2 + echo "$text" >&2 + fi + exit 1 + ;; esac done diff --git a/pyanaconda/core/configuration/system.py b/pyanaconda/core/configuration/system.py index caedd1e5fa3..b1205ce8d6d 100644 --- a/pyanaconda/core/configuration/system.py +++ b/pyanaconda/core/configuration/system.py @@ -73,6 +73,11 @@ def can_start_user_systemd(self): """Can we start the user instance of systemd?""" return self._is_boot_iso + @property + def can_start_compositor(self): + """Can we start our own Wayland session?""" + return self._is_boot_iso + @property def can_switch_tty(self): """Can we change the foreground virtual terminal?""" diff --git a/pyanaconda/display.py b/pyanaconda/display.py index 186daea8bb3..112462e2fbd 100644 --- a/pyanaconda/display.py +++ b/pyanaconda/display.py @@ -22,23 +22,22 @@ import os import time import textwrap -import pkgutil import signal +from collections import namedtuple + 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.gnome_remote_desktop 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 @@ -53,6 +52,10 @@ log = get_module_logger(__name__) stdout_log = get_stdout_logger() + +rdp_credentials = namedtuple("rdp_credentials", ["username", "password"]) + + 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." \ @@ -74,7 +77,10 @@ def start_user_systemd(): # 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"]) + # Without SYSTEMD_LOG_TARGET variable the systemd is logging directly to terminal + # bypassing stdout and stderr + childproc = util.startProgram(["/usr/lib/systemd/systemd", "--user"], + env_add={"SYSTEMD_LOG_TARGET": "journal-or-kmsg"}) WatchProcesses.watch_process(childproc, "systemd") # Set up the session bus address. Some services started by Anaconda might call @@ -85,37 +91,24 @@ def start_user_systemd(): 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): +def ask_rd_question(anaconda, message): """ Ask the user if TUI or GUI-over-RDP should be started. + Return Tuple(should use RDP, NameTuple rdp_credentials(username, password)) + + e.g.: + (True, rdp_credentials) + rdp_credentials.username + rdp_credentials.password + :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 + :return: (use_rd, rdp_credentials(username, password)) + :rtype: Tuple(bool, NameTuple(username, password)) """ App.initialize() loop = App.get_event_loop() @@ -129,19 +122,18 @@ def ask_rd_question(anaconda, grd_server, message): 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 + return (spoke.use_remote_desktop, rdp_credentials(spoke.rdp_username, spoke.rdp_password)) -def ask_for_rd_credentials(anaconda, grd_server, username=None, password=None): + +def ask_for_rd_credentials(anaconda, 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 + + :return: namedtuple rdp_credentials(username, password) """ App.initialize() loop = App.get_event_loop() @@ -153,8 +145,7 @@ def ask_for_rd_credentials(anaconda, grd_server, username=None, password=None): 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 + return rdp_credentials(spoke._username, spoke._password) def check_rd_can_be_started(anaconda): """Check if we can start an RDP session in the current environment. @@ -297,6 +288,8 @@ def setup_display(anaconda, options): anaconda.display_mode = options.display_mode anaconda.interactive_mode = not options.noninteractive + # TODO: Refactor this method or maybe whole class, ideally this class should be usable only + # on boot.iso where compositor could be set if flags.rescue_mode: return @@ -305,33 +298,37 @@ def setup_display(anaconda, options): anaconda.initialize_interface() return + # we can't start compositor so not even RDP is supported, do only base initialization + if not conf.system.can_start_compositor: + anaconda.log_display_mode() + anaconda.initialize_interface() + startup_utils.fallback_to_tui_if_gtk_ui_is_not_available(anaconda) + startup_utils.check_memory(anaconda, options) + 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 + rdp_creds = rdp_credentials("", "") 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 + rdp_creds = rdp_credentials(options.rdp_username, options.rdp_password) # note if we have both set - rdp_credentials_sufficient = options.rdp_username and options.rdp_password + if options.rdp_username and options.rdp_password: + rdp_credentials_sufficient = True + else: + rdp_credentials_sufficient = False # 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 + startup_utils.fallback_to_tui_if_gtk_ui_is_not_available(anaconda) # check if remote desktop mode can be started rd_can_be_started, rd_error_messages = check_rd_can_be_started(anaconda) @@ -341,7 +338,9 @@ def setup_display(anaconda, options): # 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) + rdp_creds = ask_for_rd_credentials(anaconda, + 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 @@ -355,9 +354,12 @@ def setup_display(anaconda, options): "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): + use_rd, credentials = ask_rd_question(anaconda, message) + if not use_rd: # user has explicitly specified text mode flags.rd_question = False + else: + rdp_creds = credentials anaconda.log_display_mode() startup_utils.check_memory(anaconda, options) @@ -403,11 +405,14 @@ def on_mutter_ready(observer): "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) + rdp_creds = ask_rd_question(anaconda, 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 = GRDServer(anaconda) # The RDP server object + grd_server.rdp_username = rdp_creds.username + grd_server.rdp_password = rdp_creds.password grd_server.start_grd_rdp() # with Wayland running we can initialize the UI interface diff --git a/pyanaconda/gnome_remote_destop.py b/pyanaconda/gnome_remote_desktop.py similarity index 100% rename from pyanaconda/gnome_remote_destop.py rename to pyanaconda/gnome_remote_desktop.py diff --git a/pyanaconda/startup_utils.py b/pyanaconda/startup_utils.py index fc68f35ec41..4a8aec612b9 100644 --- a/pyanaconda/startup_utils.py +++ b/pyanaconda/startup_utils.py @@ -20,6 +20,7 @@ import sys import time import os +import pkgutil from blivet.arch import is_s390 from blivet.util import total_memory from dasbus.typing import get_variant, Int @@ -174,6 +175,22 @@ def set_storage_checker_minimal_ram_size(display_mode): ) +def fallback_to_tui_if_gtk_ui_is_not_available(anaconda): + """Check if GTK UI is available in this environment and fallback to TUI if not. + + Also take into account Web UI. + """ + if anaconda.gui_mode and not anaconda.is_webui_supported: + import pyanaconda.ui + + 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 = DisplayModes.TUI + flags.use_rd = False + flags.rd_question = False + + def setup_logging_from_options(options): """Configure logging according to Anaconda command line/boot options. diff --git a/tests/unit_tests/pyanaconda_tests/core/test_startup_utils.py b/tests/unit_tests/pyanaconda_tests/core/test_startup_utils.py index c027e245654..2c2156f221c 100644 --- a/tests/unit_tests/pyanaconda_tests/core/test_startup_utils.py +++ b/tests/unit_tests/pyanaconda_tests/core/test_startup_utils.py @@ -26,8 +26,10 @@ from textwrap import dedent from pyanaconda.startup_utils import print_dracut_errors, check_if_geolocation_should_be_used, \ - start_geolocation_conditionally, wait_for_geolocation_and_use, apply_geolocation_result -from pyanaconda.core.constants import GEOLOC_CONNECTION_TIMEOUT, TIMEZONE_PRIORITY_GEOLOCATION + start_geolocation_conditionally, wait_for_geolocation_and_use, apply_geolocation_result, \ + fallback_to_tui_if_gtk_ui_is_not_available +from pyanaconda.core.constants import GEOLOC_CONNECTION_TIMEOUT, TIMEZONE_PRIORITY_GEOLOCATION, \ + DisplayModes from pyanaconda.modules.common.structures.timezone import GeolocationData class StartupUtilsTestCase(unittest.TestCase): @@ -303,3 +305,63 @@ def test_apply_tz_missing(self, has_trans_mock, setup_locale_mock, geodata_mock, assert tz_proxy.Timezone == "" setup_locale_mock.assert_called_once_with("es_ES.UTF-8", loc_proxy, text_mode=False) assert os.environ == {"LANG": "es_ES.UTF-8"} + + +class TestUIHelpers(unittest.TestCase): + + @patch("pyanaconda.startup_utils.pkgutil") + @patch("pyanaconda.startup_utils.flags") + def test_fallback_tui_when_gtk_ui_not_available(self, mocked_flags, mocked_pkgutil): + mocked_anaconda = Mock() + + def check_method(gui_mode, + webui_supported, + gtk_available, + expected_display_mode, + expected_rd_output): + mocked_anaconda.gui_mode = gui_mode + mocked_anaconda.is_webui_supported = webui_supported + + # prefilled values + mocked_anaconda.display_mode = "" + mocked_flags.use_rd = None + mocked_flags.rd_question = None + + if gtk_available: + mocked_pkgutil.iter_modules.return_value = [(None, "pyanaconda.ui.gui")] + else: + mocked_pkgutil.iter_modules.return_value = [(None, "pyanaconda.ui.webui")] + + fallback_to_tui_if_gtk_ui_is_not_available(mocked_anaconda) + + assert mocked_flags.use_rd is expected_rd_output + assert mocked_flags.rd_question is expected_rd_output + assert mocked_anaconda.display_mode == expected_display_mode + + # UI is not wanted + check_method(gui_mode=False, + webui_supported=False, + gtk_available=True, + expected_display_mode="", + expected_rd_output=None) + + # check result when web ui is supported + check_method(gui_mode=True, + webui_supported=True, + gtk_available=True, + expected_display_mode="", + expected_rd_output=None) + + # check result when gtk UI is not available + check_method(gui_mode=True, + webui_supported=False, + gtk_available=False, + expected_display_mode=DisplayModes.TUI, + expected_rd_output=False) + + # check result when GTK is available + check_method(gui_mode=True, + webui_supported=False, + gtk_available=True, + expected_display_mode="", + expected_rd_output=None) diff --git a/tests/unit_tests/pyanaconda_tests/test_display.py b/tests/unit_tests/pyanaconda_tests/test_display.py index adf5b37ca59..231061b063a 100644 --- a/tests/unit_tests/pyanaconda_tests/test_display.py +++ b/tests/unit_tests/pyanaconda_tests/test_display.py @@ -45,7 +45,8 @@ def test_start_user_systemd(self, util_mock, conf_mock, watch_mock): start_user_systemd() util_mock.startProgram.assert_called_once_with( - ["/usr/lib/systemd/systemd", "--user"] + ["/usr/lib/systemd/systemd", "--user"], + env_add={"SYSTEMD_LOG_TARGET": "journal-or-kmsg"} ) watch_mock.watch_process.assert_called_once_with( 100, "systemd"