Skip to content

Commit

Permalink
Add wayland support
Browse files Browse the repository at this point in the history
  • Loading branch information
Dramelac committed Jan 31, 2024
1 parent 6c72231 commit fefce65
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 38 deletions.
17 changes: 7 additions & 10 deletions exegol/config/EnvInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import platform
from enum import Enum
from pathlib import Path
from typing import Optional, Any, List

from exegol.config.ConstantConfig import ConstantConfig
Expand All @@ -17,7 +18,7 @@ class HostOs(Enum):
WINDOWS = "Windows"
LINUX = "Linux"
MAC = "Mac"

class DisplayServer(Enum):
"""Dictionary class for static Display Server"""
WAYLAND = "Wayland"
Expand Down Expand Up @@ -117,13 +118,14 @@ def getHostOs(cls) -> HostOs:
def getDisplayServer(cls) -> DisplayServer:
"""Returns the display server
Can be 'X11' or 'Wayland'"""
if "wayland" in os.getenv("XDG_SESSION_TYPE"):
session_type = os.getenv("XDG_SESSION_TYPE", "")
if session_type == "wayland":
return cls.DisplayServer.WAYLAND
elif "x11" in os.getenv("XDG_SESSION_TYPE"):
elif session_type == "x11":
return cls.DisplayServer.X11
else:
# Should return an error
return os.getenv("XDG_SESSION_TYPE")
return session_type

@classmethod
def getWindowsRelease(cls) -> str:
Expand All @@ -147,12 +149,7 @@ def isMacHost(cls) -> bool:
return cls.getHostOs() == cls.HostOs.MAC

@classmethod
def isX11(cls) -> bool:
"""Return true if x11 is detected on the host"""
return cls.getDisplayServer() == cls.DisplayServer.X11

@classmethod
def isWayland(cls) -> bool:
def isWaylandAvailable(cls) -> bool:
"""Return true if wayland is detected on the host"""
return cls.getDisplayServer() == cls.DisplayServer.WAYLAND

Expand Down
2 changes: 1 addition & 1 deletion exegol/console/TUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ def __buildContainerRecapTable(container: ExegolContainerTemplate):
recap.add_row("[bold blue]Desktop[/bold blue]", container.config.getDesktopConfig())
if creation_date:
recap.add_row("[bold blue]Creation date[/bold blue]", creation_date)
recap.add_row("[bold blue]X11[/bold blue]", boolFormatter(container.config.isGUIEnable()))
recap.add_row("[bold blue]GUI[/bold blue]", boolFormatter(container.config.isGUIEnable()) + container.config.getTextGuiSockets())
recap.add_row("[bold blue]Network[/bold blue]", container.config.getTextNetworkMode())
recap.add_row("[bold blue]Timezone[/bold blue]", boolFormatter(container.config.isTimezoneShared()))
recap.add_row("[bold blue]Exegol resources[/bold blue]", boolFormatter(container.config.isExegolResourcesEnable()) +
Expand Down
63 changes: 45 additions & 18 deletions exegol/model/ContainerConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(self, container: Optional[Container] = None):
"""Container config default value"""
self.hostname = ""
self.__enable_gui: bool = False
self.__gui_engine: List[str] = []
self.__share_timezone: bool = False
self.__my_resources: bool = False
self.__my_resources_path: str = "/opt/my-resources"
Expand Down Expand Up @@ -130,10 +131,13 @@ def __parseContainerConfig(self, container: Container):
self.interactive = container_config.get("OpenStdin", True)
self.legacy_entrypoint = container_config.get("Entrypoint") is None
self.__enable_gui = False
for env in self.__envs:
if "DISPLAY" in env:
self.__enable_gui = True
break
envs_key = self.__envs.keys()
if "DISPLAY" in envs_key:
self.__enable_gui = True
self.__gui_engine.append("X11")
if "WAYLAND_DISPLAY" in envs_key:
self.__enable_gui = True
self.__gui_engine.append("Wayland")

# Host Config section
host_config = container.attrs.get("HostConfig", {})
Expand Down Expand Up @@ -365,15 +369,35 @@ def enableGUI(self):
return
if not self.__enable_gui:
logger.verbose("Config: Enabling display sharing")
x11_enable = False
wayland_enable = False
try:
host_path = GuiUtils.getX11SocketPath()
if host_path is not None:
self.addVolume(host_path, GuiUtils.default_x11_path, must_exist=True)
self.addEnv("DISPLAY", GuiUtils.getDisplayEnv())
self.__gui_engine.append("X11")
x11_enable = True
except CancelOperation as e:
logger.warning(f"Graphical interface sharing could not be enabled: {e}")
logger.warning(f"Graphical X11 interface sharing could not be enabled: {e}")
try:
if EnvInfo.isWaylandAvailable():
host_path = GuiUtils.getWaylandSocketPath()
if host_path is not None:
self.addVolume(host_path.as_posix(), f"/tmp/{host_path.name}", must_exist=True)
self.addEnv("XDG_SESSION_TYPE", "wayland")
self.addEnv("XDG_RUNTIME_DIR", "/tmp")
self.addEnv("WAYLAND_DISPLAY", GuiUtils.getWaylandEnv())
self.__gui_engine.append("Wayland")
wayland_enable = True
except CancelOperation as e:
logger.warning(f"Graphical Wayland interface sharing could not be enabled: {e}")
if not wayland_enable and not x11_enable:
return
elif not x11_enable:
# Only wayland setup
logger.warning("X11 cannot be shared, only wayland, some graphical applications might not work...")
# TODO support pulseaudio
self.addEnv("DISPLAY", GuiUtils.getDisplayEnv())
for k, v in self.__static_gui_envs.items():
self.addEnv(k, v)
self.__enable_gui = True
Expand All @@ -385,8 +409,12 @@ def __disableGUI(self):
logger.verbose("Config: Disabling display sharing")
self.removeVolume(container_path="/tmp/.X11-unix")
self.removeEnv("DISPLAY")
self.removeEnv("XDG_SESSION_TYPE")
self.removeEnv("XDG_RUNTIME_DIR")
self.removeEnv("WAYLAND_DISPLAY")
for k in self.__static_gui_envs.keys():
self.removeEnv(k)
self.__gui_engine.clear()

def enableSharedTimezone(self):
"""Procedure to enable shared timezone feature"""
Expand Down Expand Up @@ -980,7 +1008,7 @@ def addVolume(self,
# if force_sticky_group is set, user choice is bypassed, fs will be updated.
execute_update_fs = force_sticky_group or (enable_sticky_group and (UserConfig().auto_update_workspace_fs ^ ParametersManager().update_fs_perms))
try:
if not (path.is_file() or path.is_dir()):
if not path.exists():
if must_exist:
raise CancelOperation(f"{host_path} does not exist on your host.")
else:
Expand Down Expand Up @@ -1080,17 +1108,10 @@ def getShellEnvs(self) -> List[str]:
result = []
# Select default shell to use
result.append(f"{self.ExegolEnv.user_shell.value}={ParametersManager().shell}")
# Manage the GUI
# Update X11 DISPLAY socket if needed
if self.__enable_gui:
current_display = GuiUtils.getDisplayEnv()

# Wayland
if EnvInfo.isWayland():
result.append(f"WAYLAND_DISPLAY={current_display}")
result.append(f"XDG_RUNTIME_DIR=/tmp")

# Share X11 (GUI Display) config

# If the default DISPLAY environment in the container is not the same as the DISPLAY of the user's session,
# the environment variable will be updated in the exegol shell.
if current_display and self.__envs.get('DISPLAY', '') != current_display:
Expand Down Expand Up @@ -1293,7 +1314,7 @@ def getTextFeatures(self, verbose: bool = False) -> str:
if verbose or self.isDesktopEnabled():
result += f"{getColor(self.isDesktopEnabled())[0]}Desktop: {self.getDesktopConfig()}{getColor(self.isDesktopEnabled())[1]}{os.linesep}"
if verbose or not self.__enable_gui:
result += f"{getColor(self.__enable_gui)[0]}X11: {boolFormatter(self.__enable_gui)}{getColor(self.__enable_gui)[1]}{os.linesep}"
result += f"{getColor(self.__enable_gui)[0]}GUI: {boolFormatter(self.__enable_gui)}{getColor(self.__enable_gui)[1]}{os.linesep}"
if verbose or not self.__network_host:
result += f"[green]Network mode: [/green]{self.getTextNetworkMode()}{os.linesep}"
if self.__vpn_path is not None:
Expand Down Expand Up @@ -1325,6 +1346,12 @@ def getDesktopConfig(self) -> str:
f"{'localhost' if self.__desktop_host == '127.0.0.1' else self.__desktop_host}:{self.__desktop_port}")
return f"[link={config}][deep_sky_blue3]{config}[/deep_sky_blue3][/link]"

def getTextGuiSockets(self):
if self.__enable_gui:
return f"[bright_black]({' + '.join(self.__gui_engine)})[/bright_black]"
else:
return ""

def getTextNetworkMode(self) -> str:
"""Network mode, text getter"""
network_mode = "host" if self.__network_host else "bridge"
Expand All @@ -1344,7 +1371,7 @@ def getTextMounts(self, verbose: bool = False) -> str:
result = ''
hidden_mounts = ['/tmp/.X11-unix', '/opt/resources', '/etc/localtime',
'/etc/timezone', '/my-resources', '/opt/my-resources',
'/.exegol/entrypoint.sh', '/.exegol/spawn.sh']
'/.exegol/entrypoint.sh', '/.exegol/spawn.sh', '/tmp/wayland-0', '/tmp/wayland-1']
for mount in self.__mounts:
# Not showing technical mounts
if not verbose and mount.get('Target') in hidden_mounts:
Expand Down Expand Up @@ -1373,7 +1400,7 @@ def getTextEnvs(self, verbose: bool = False) -> str:
result = ''
for k, v in self.__envs.items():
# Blacklist technical variables, only shown in verbose
if not verbose and k in list(self.__static_gui_envs.keys()) + [v.value for v in self.ExegolEnv] + ["DISPLAY", "PATH"]:
if not verbose and k in list(self.__static_gui_envs.keys()) + [v.value for v in self.ExegolEnv] + ["DISPLAY", "WAYLAND_DISPLAY", "XDG_SESSION_TYPE", "XDG_RUNTIME_DIR", "PATH"]:
continue
result += f"{k}={v}{os.linesep}"
return result
Expand Down
30 changes: 21 additions & 9 deletions exegol/utils/GuiUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,23 @@ def getX11SocketPath(cls) -> Optional[str]:
return cls.default_x11_path

@classmethod
def getDisplayEnv(cls) -> str:
def getWaylandSocketPath(cls) -> Optional[Path]:
"""
Get the current DISPLAY environment to access the display server
Get the host path of the Wayland socket
:return:
"""
if EnvInfo.isWayland():
# Wayland
return os.getenv('WAYLAND_DISPLAY', 'wayland-1')

if EnvInfo.isX11():
# X11
return os.getenv('DISPLAY', ":0")
wayland_dir = os.getenv("XDG_RUNTIME_DIR")
wayland_socket = os.getenv("WAYLAND_DISPLAY")
if wayland_dir is None or wayland_socket is None:
return None
return Path(wayland_dir + os.sep + wayland_socket)

@classmethod
def getDisplayEnv(cls) -> str:
"""
Get the current DISPLAY environment to access X11 socket
:return:
"""
if EnvInfo.isMacHost():
# xquartz Mac mode
return "host.docker.internal:0"
Expand All @@ -85,6 +89,14 @@ def getDisplayEnv(cls) -> str:
# DISPLAY var is fetch from the current user environment. If it doesn't exist, using ':0'.
return os.getenv('DISPLAY', ":0")

@classmethod
def getWaylandEnv(cls) -> str:
"""
Get the current WAYLAND_DISPLAY environment to access wayland socket
:return:
"""
return os.getenv('WAYLAND_DISPLAY', 'wayland-0')

# # # # # # Mac specific methods # # # # # #

@classmethod
Expand Down

0 comments on commit fefce65

Please sign in to comment.