Skip to content

Commit

Permalink
Merge pull request #206 from ThePorgs/feat/wayland
Browse files Browse the repository at this point in the history
Add Feat/wayland to dev branch
  • Loading branch information
Dramelac authored Feb 26, 2024
2 parents e144b3b + 98d26fa commit 6bfe086
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 17 deletions.
26 changes: 26 additions & 0 deletions exegol/config/EnvInfo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
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,6 +19,11 @@ class HostOs(Enum):
LINUX = "Linux"
MAC = "Mac"

class DisplayServer(Enum):
"""Dictionary class for static Display Server"""
WAYLAND = "Wayland"
X11 = "X11"

class DockerEngine(Enum):
"""Dictionary class for static Docker engine name"""
WLS2 = "WSL2"
Expand Down Expand Up @@ -107,6 +114,20 @@ def getHostOs(cls) -> HostOs:
assert cls.__docker_host_os is not None
return cls.__docker_host_os

@classmethod
def getDisplayServer(cls) -> DisplayServer:
"""Returns the display server
Can be 'X11' or 'Wayland'"""
session_type = os.getenv("XDG_SESSION_TYPE", "x11")
if session_type == "wayland":
return cls.DisplayServer.WAYLAND
elif session_type == "x11":
return cls.DisplayServer.X11
else:
# Should return an error
logger.warning(f"Unknown session type {session_type}. Using X11 as fallback.")
return cls.DisplayServer.X11

@classmethod
def getWindowsRelease(cls) -> str:
# Cache check
Expand All @@ -128,6 +149,11 @@ def isMacHost(cls) -> bool:
"""Return true if macOS is detected on the host"""
return cls.getHostOs() == cls.HostOs.MAC

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

@classmethod
def isDockerDesktop(cls) -> bool:
"""Return true if docker desktop is used on the host"""
Expand Down
4 changes: 2 additions & 2 deletions exegol/console/TUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,10 @@ def __buildContainerRecapTable(container: ExegolContainerTemplate):
recap.add_row("[bold blue]Comment[/bold blue]", comment)
if passwd:
recap.add_row(f"[bold blue]Credentials[/bold blue]", f"[deep_sky_blue3]{container.config.getUsername()}[/deep_sky_blue3] : [deep_sky_blue3]{passwd}[/deep_sky_blue3]")
recap.add_row("[bold blue]Desktop[/bold blue]", container.config.getDesktopConfig())
recap.add_row("[bold blue]Remote 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]Console 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
62 changes: 49 additions & 13 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,36 @@ 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()
host_path: Optional[Union[Path, str]] = GuiUtils.getX11SocketPath()
if host_path is not None:
assert type(host_path) is str
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 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 interface sharing could not be enabled: {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 +410,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 +1009,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,9 +1109,10 @@ def getShellEnvs(self) -> List[str]:
result = []
# Select default shell to use
result.append(f"{self.ExegolEnv.user_shell.value}={ParametersManager().shell}")
# Share X11 (GUI Display) config
# Update X11 DISPLAY socket if needed
if self.__enable_gui:
current_display = GuiUtils.getDisplayEnv()

# 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 @@ -1283,9 +1313,9 @@ def getTextFeatures(self, verbose: bool = False) -> str:
if verbose or self.__privileged:
result += f"{getColor(not self.__privileged)[0]}Privileged: {'On :fire:' if self.__privileged else '[green]Off :heavy_check_mark:[/green]'}{getColor(not self.__privileged)[1]}{os.linesep}"
if verbose or self.isDesktopEnabled():
result += f"{getColor(self.isDesktopEnabled())[0]}Desktop: {self.getDesktopConfig()}{getColor(self.isDesktopEnabled())[1]}{os.linesep}"
result += f"{getColor(self.isDesktopEnabled())[0]}Remote 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]}Console 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 @@ -1317,6 +1347,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 @@ -1336,7 +1372,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 @@ -1365,7 +1401,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
6 changes: 5 additions & 1 deletion exegol/model/ExegolContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ def __start_container(self):
"""
with console.status(f"Waiting to start {self.name}", spinner_style="blue") as progress:
start_date = datetime.utcnow()
self.__container.start()
try:
self.__container.start()
except APIError as e:
logger.debug(e)
logger.critical(f"Docker raise a critical error when starting the container [green]{self.name}[/green], error message is: {e.explanation}")
if not self.config.legacy_entrypoint: # TODO improve startup compatibility check
try:
# Try to find log / startup messages. Will time out after 2 seconds if the image don't support status update through container logs.
Expand Down
22 changes: 21 additions & 1 deletion exegol/utils/GuiUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,22 @@ def getX11SocketPath(cls) -> Optional[str]:
# Other distributions (Linux / Mac) have the default socket path
return cls.default_x11_path

@classmethod
def getWaylandSocketPath(cls) -> Optional[Path]:
"""
Get the host path of the Wayland socket
:return:
"""
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, wayland_socket)

@classmethod
def getDisplayEnv(cls) -> str:
"""
Get the current DISPLAY env to access X11 socket
Get the current DISPLAY environment to access X11 socket
:return:
"""
if EnvInfo.isMacHost():
Expand All @@ -77,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 6bfe086

Please sign in to comment.