Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Feat/wayland to dev branch #206

Merged
merged 5 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading