Skip to content

Commit

Permalink
Drop xrandr
Browse files Browse the repository at this point in the history
Anaconda uses xrandr to set the screen resolution when the boot option
"inst.resolution" [1] is used.

In order to be able to drop the X.Org server, use Mutter's API instead
of xrandr.

The kickstart equivalent option has been removed, so we don't need to
care about it [2].

[1] https://anaconda-installer.readthedocs.io/en/latest/boot-options.html#inst-resolution
[2] https://pykickstart.readthedocs.io/en/latest/kickstart-docs.html#xconfig

Resolves: RHEL-38399
  • Loading branch information
jexposit authored and M4rtinK committed May 27, 2024
1 parent 602ff38 commit a85523f
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 18 deletions.
1 change: 0 additions & 1 deletion anaconda.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,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
Expand Down
3 changes: 3 additions & 0 deletions pyanaconda/core/regexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]*$')
34 changes: 18 additions & 16 deletions pyanaconda/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -218,28 +219,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()
Expand Down Expand Up @@ -390,7 +384,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 "
Expand Down
9 changes: 8 additions & 1 deletion pyanaconda/modules/common/constants/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pyanaconda.core.dbus import SystemBus, DBus
from pyanaconda.core.dbus import SystemBus, SessionBus, DBus
from dasbus.identifier import DBusServiceIdentifier
from pyanaconda.modules.common.constants.namespaces import BOSS_NAMESPACE, TIMEZONE_NAMESPACE, \
NETWORK_NAMESPACE, LOCALIZATION_NAMESPACE, SECURITY_NAMESPACE, USERS_NAMESPACE, \
Expand Down Expand Up @@ -107,3 +107,10 @@
namespace=NETWORK_MANAGER_NAMESPACE,
message_bus=SystemBus
)

# Session services.

MUTTER_DISPLAY_CONFIG = DBusServiceIdentifier(
namespace=("org", "gnome", "Mutter", "DisplayConfig"),
message_bus=SessionBus
)
171 changes: 171 additions & 0 deletions pyanaconda/mutter_display.py
Original file line number Diff line number Diff line change
@@ -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))

0 comments on commit a85523f

Please sign in to comment.