diff --git a/anaconda.spec.in b/anaconda.spec.in index 53bb3c61dc22..f454206e6be2 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -249,7 +249,6 @@ Requires: createrepo_c # Display stuff moved from lorax templates Requires: xorg-x11-drivers Requires: xorg-x11-server-Xorg -Requires: xrandr Requires: xrdb Requires: dbus-x11 Requires: gsettings-desktop-schemas diff --git a/pyanaconda/core/regexes.py b/pyanaconda/core/regexes.py index cc00702e3af6..e956b2ddec47 100644 --- a/pyanaconda/core/regexes.py +++ b/pyanaconda/core/regexes.py @@ -197,3 +197,6 @@ # Name of initramfs connection created by NM based on MAC NM_MAC_INITRAMFS_CONNECTION = re.compile(r'^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$') + +# Screen resolution format for the boot option "inst.resolution" +SCREEN_RESOLUTION_CONFIG = re.compile(r'^[0-9]*x[0-9]*$') diff --git a/pyanaconda/display.py b/pyanaconda/display.py index 7713a6ce5ea9..fb46cfba3251 100644 --- a/pyanaconda/display.py +++ b/pyanaconda/display.py @@ -26,6 +26,7 @@ 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 @@ -220,28 +221,21 @@ def x11_preexec(): WatchProcesses.watch_process(childproc, "gnome-kiosk") -def set_x_resolution(runres): - """Set X server screen resolution. +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) - util.execWithRedirect("xrandr", ["-d", ":1", "-s", runres]) - except RuntimeError: - log.error("The X resolution was not set") - util.execWithRedirect("xrandr", ["-d", ":1", "-q"]) + mutter_display = MutterDisplay() + mutter_display.set_resolution(runres) + except MutterConfigError as error: + log.error("The resolution was not set: %s", error) -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) - +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_spice_vd_agent() @@ -392,7 +386,15 @@ def setup_display(anaconda, options): time.sleep(2) if not anaconda.gui_startup_failed: - do_extra_x11_actions(options.runres, gui_mode=anaconda.gui_mode) + do_extra_x11_actions() + + if options.runres and anaconda.gui_mode and not flags.usevnc: + 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.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 " diff --git a/pyanaconda/modules/common/constants/services.py b/pyanaconda/modules/common/constants/services.py index afcd3e946904..88b3e6f41d20 100644 --- a/pyanaconda/modules/common/constants/services.py +++ b/pyanaconda/modules/common/constants/services.py @@ -114,3 +114,8 @@ namespace=("org", "gnome", "Kiosk"), message_bus=SessionBus ) + +MUTTER_DISPLAY_CONFIG = DBusServiceIdentifier( + namespace=("org", "gnome", "Mutter", "DisplayConfig"), + message_bus=SessionBus +) diff --git a/pyanaconda/mutter_display.py b/pyanaconda/mutter_display.py new file mode 100644 index 000000000000..d06f216e90ac --- /dev/null +++ b/pyanaconda/mutter_display.py @@ -0,0 +1,171 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# + +from dasbus.client.observer import DBusObserver +from pyanaconda.core.dbus import SessionBus +from pyanaconda.modules.common.constants.services import MUTTER_DISPLAY_CONFIG +from pyanaconda.core.regexes import SCREEN_RESOLUTION_CONFIG + + +__all__ = ['MutterDisplay', 'MutterConfigError'] + + +class MutterConfigError(Exception): + """Exception class for mutter configuration related problems""" + pass + + +class MonitorId(object): + """Collection of properties that identify a unique monitor.""" + + def __init__(self, props): + self.connector = props[0] + self.vendor = props[1] + self.product = props[2] + self.serial = props[3] + + def __eq__(self, other): + return self.connector == other.connector and \ + self.vendor == other.vendor and \ + self.product == other.product and \ + self.serial == other.serial + + +class MonitorMode(object): + """Available modes for a monitor.""" + + def __init__(self, props): + self.id = props[0] + self.width = props[1] + self.height = props[2] + self.refresh_rate = props[3] + self.preferred_scale = props[4] + self.supported_scales = props[5] + self.properties = props[6] + + +class Monitor(object): + """Represent a connected physical monitor.""" + + def __init__(self, props): + self.id = MonitorId(props[0]) + self.modes = list(map(MonitorMode, props[1])) + self.properties = props[2] + + +class LogicalMonitor(object): + """Represent the current logical monitor configuration""" + + def __init__(self, props): + self.x = props[0] + self.y = props[1] + self.scale = props[2] + self.transform = props[3] + self.primary = props[4] + self.monitor_ids = list(map(MonitorId, props[5])) + self.properties = props[6] + + +class LogicalMonitorConfig(object): + """Logical monitor configuration object""" + + def __init__(self, logical_monitor, monitors, x, y, width, height): + """Creates a LogicalMonitorConfig setting the given resolution if available.""" + self._logical_monitor = logical_monitor + self._monitors = monitors + + self.x = x + self.y = y + self.scale = logical_monitor.scale + self.transform = logical_monitor.transform + self.primary = logical_monitor.primary + + self.monitors = list() + for monitor_id in logical_monitor.monitor_ids: + connector = monitor_id.connector + mode_id = self._get_matching_monitor_mode_id(monitors, monitor_id, width, height) + self.monitors.append((connector, mode_id, {})) + + def _get_matching_monitor_mode_id(self, monitors, monitor_id, width, height): + monitor = next(filter(lambda m: m.id == monitor_id, monitors)) + for mode in monitor.modes: + if mode.width == width and mode.height == height: + return mode.id + + raise MutterConfigError('Monitor mode with selected resolution not found') + + def to_dbus(self): + return ( + self.x, + self.y, + self.scale, + self.transform, + self.primary, + self.monitors, + ) + + +class MutterDisplay(object): + """Class wrapping Mutter's display configuration API.""" + + def __init__(self): + self._proxy = MUTTER_DISPLAY_CONFIG.get_proxy() + + def on_service_ready(self, callback): + observer = DBusObserver(SessionBus, 'org.gnome.Kiosk') + observer.service_available.connect(callback) + observer.connect_once_available() + + def set_resolution(self, res_str): + """Changes the screen resolution. + + :param res_str: Screen resolution configuration with format "800x600". + :raises MutterConfigError on failure. + """ + if not self._proxy.ApplyMonitorsConfigAllowed: + raise MutterConfigError('Monitor configuration is not allowed') + + (width, height) = self._parse_resolution_str(res_str) + (serial, monitor_props, logical_monitor_props, _) = self._proxy.GetCurrentState() + + # Configuration method as described in org.gnome.Mutter.DisplayConfig.xml: + # 0: verify + # 1: temporary + # 2: persistent + persistent_config = 2 + + monitors = list(map(Monitor, monitor_props)) + logical_monitors = list(map(LogicalMonitor, logical_monitor_props)) + + # Align the monitors in a row starting at X coordinate 0 + x = 0 + + configs = list() + for logical_monitor in logical_monitors: + config = LogicalMonitorConfig(logical_monitor, monitors, x, 0, width, height) + x += width + configs.append(config.to_dbus()) + + self._proxy.ApplyMonitorsConfig(serial, persistent_config, configs, {}) + + def _parse_resolution_str(self, res_str): + if not SCREEN_RESOLUTION_CONFIG.match(res_str): + raise MutterConfigError('Invalid configuration resolution') + + [width, height] = res_str.split('x') + return (int(width, 10), int(height, 10))