From fc8b82baa6c5f035761aea44084d39ec10ff4509 Mon Sep 17 00:00:00 2001 From: QU35T-code Date: Fri, 26 Jan 2024 22:51:36 +0100 Subject: [PATCH 1/4] Added wayland (environment) support --- exegol/config/EnvInfo.py | 28 ++++++++++++++++++++++++++++ exegol/model/ContainerConfig.py | 10 +++++++++- exegol/utils/GuiUtils.py | 10 +++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/exegol/config/EnvInfo.py b/exegol/config/EnvInfo.py index d0eb1a17..1522e519 100644 --- a/exegol/config/EnvInfo.py +++ b/exegol/config/EnvInfo.py @@ -1,4 +1,5 @@ import json +import os import platform from enum import Enum from typing import Optional, Any, List @@ -16,6 +17,11 @@ class HostOs(Enum): WINDOWS = "Windows" 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""" @@ -107,6 +113,18 @@ 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'""" + if "wayland" in os.getenv("XDG_SESSION_TYPE"): + return cls.DisplayServer.WAYLAND + elif "x11" in os.getenv("XDG_SESSION_TYPE"): + return cls.DisplayServer.X11 + else: + # Should return an error + return os.getenv("XDG_SESSION_TYPE") + @classmethod def getWindowsRelease(cls) -> str: # Cache check @@ -128,6 +146,16 @@ def isMacHost(cls) -> bool: """Return true if macOS is detected on the host""" 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: + """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""" diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 154fef5f..b9cc8aa1 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -1080,9 +1080,17 @@ 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 + # Manage the GUI 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: diff --git a/exegol/utils/GuiUtils.py b/exegol/utils/GuiUtils.py index 62ee1f95..d3d0229f 100644 --- a/exegol/utils/GuiUtils.py +++ b/exegol/utils/GuiUtils.py @@ -61,9 +61,17 @@ def getX11SocketPath(cls) -> Optional[str]: @classmethod def getDisplayEnv(cls) -> str: """ - Get the current DISPLAY env to access X11 socket + Get the current DISPLAY environment to access the display server :return: """ + if EnvInfo.isWayland(): + # Wayland + return os.getenv('WAYLAND_DISPLAY', 'wayland-1') + + if EnvInfo.isX11(): + # X11 + return os.getenv('DISPLAY', ":0") + if EnvInfo.isMacHost(): # xquartz Mac mode return "host.docker.internal:0" From fefce650e0695d9c43190b2e9100ea105eba9e05 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Wed, 31 Jan 2024 21:34:27 +0100 Subject: [PATCH 2/4] Add wayland support --- exegol/config/EnvInfo.py | 17 ++++----- exegol/console/TUI.py | 2 +- exegol/model/ContainerConfig.py | 63 +++++++++++++++++++++++---------- exegol/utils/GuiUtils.py | 30 +++++++++++----- 4 files changed, 74 insertions(+), 38 deletions(-) diff --git a/exegol/config/EnvInfo.py b/exegol/config/EnvInfo.py index 1522e519..7d2c5db7 100644 --- a/exegol/config/EnvInfo.py +++ b/exegol/config/EnvInfo.py @@ -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 @@ -17,7 +18,7 @@ class HostOs(Enum): WINDOWS = "Windows" LINUX = "Linux" MAC = "Mac" - + class DisplayServer(Enum): """Dictionary class for static Display Server""" WAYLAND = "Wayland" @@ -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: @@ -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 diff --git a/exegol/console/TUI.py b/exegol/console/TUI.py index e1469b82..e977571f 100644 --- a/exegol/console/TUI.py +++ b/exegol/console/TUI.py @@ -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()) + diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index dfb515ec..d1e3cab4 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -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" @@ -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", {}) @@ -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 @@ -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""" @@ -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: @@ -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: @@ -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: @@ -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" @@ -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: @@ -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 diff --git a/exegol/utils/GuiUtils.py b/exegol/utils/GuiUtils.py index d3d0229f..37856fcb 100644 --- a/exegol/utils/GuiUtils.py +++ b/exegol/utils/GuiUtils.py @@ -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" @@ -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 From 7e41e6ea0f275a0e7efb8fec75525c8c31b130e5 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Sun, 25 Feb 2024 17:39:25 +0100 Subject: [PATCH 3/4] Update info text + default to X11 --- exegol/config/EnvInfo.py | 5 +++-- exegol/console/TUI.py | 4 ++-- exegol/model/ContainerConfig.py | 7 ++++--- exegol/utils/GuiUtils.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/exegol/config/EnvInfo.py b/exegol/config/EnvInfo.py index 7d2c5db7..774ca098 100644 --- a/exegol/config/EnvInfo.py +++ b/exegol/config/EnvInfo.py @@ -118,14 +118,15 @@ def getHostOs(cls) -> HostOs: def getDisplayServer(cls) -> DisplayServer: """Returns the display server Can be 'X11' or 'Wayland'""" - session_type = os.getenv("XDG_SESSION_TYPE", "") + 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 - return session_type + logger.warning(f"Unknown session type {session_type}. Using X11 as fallback.") + return cls.DisplayServer.X11 @classmethod def getWindowsRelease(cls) -> str: diff --git a/exegol/console/TUI.py b/exegol/console/TUI.py index e977571f..893d4cbe 100644 --- a/exegol/console/TUI.py +++ b/exegol/console/TUI.py @@ -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]GUI[/bold blue]", boolFormatter(container.config.isGUIEnable()) + container.config.getTextGuiSockets()) + 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()) + diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index d1e3cab4..1987f989 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -372,8 +372,9 @@ def enableGUI(self): 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") @@ -1312,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]}GUI: {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: diff --git a/exegol/utils/GuiUtils.py b/exegol/utils/GuiUtils.py index 37856fcb..f6f8d194 100644 --- a/exegol/utils/GuiUtils.py +++ b/exegol/utils/GuiUtils.py @@ -68,7 +68,7 @@ def getWaylandSocketPath(cls) -> Optional[Path]: 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) + return Path(wayland_dir, wayland_socket) @classmethod def getDisplayEnv(cls) -> str: From 98d26fa16faeb3fb443f767b4277b46ebb594cf3 Mon Sep 17 00:00:00 2001 From: Dramelac Date: Sun, 25 Feb 2024 17:44:01 +0100 Subject: [PATCH 4/4] Handle docker critical error on start --- exegol/model/ExegolContainer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py index 57ac0eaa..333685d4 100644 --- a/exegol/model/ExegolContainer.py +++ b/exegol/model/ExegolContainer.py @@ -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.