diff --git a/.github/workflows/sub_testing.yml b/.github/workflows/sub_testing.yml index cd7db48c..15c201b1 100644 --- a/.github/workflows/sub_testing.yml +++ b/.github/workflows/sub_testing.yml @@ -16,7 +16,7 @@ jobs: with: python-version: "3.12" - name: Install requirements - run: python -m pip install --user mypy types-requests types-PyYAML + run: python -m pip install --user mypy types-requests types-PyYAML types-tzlocal - name: Run code analysis (package) run: mypy ./exegol/ --ignore-missing-imports --check-untyped-defs --pretty # TODO add --disallow-untyped-defs - name: Run code analysis (source) @@ -42,6 +42,6 @@ jobs: with: python-version: "3.12" - name: Install requirements - run: python -m pip install --user mypy types-requests types-PyYAML + run: python -m pip install --user mypy types-requests types-PyYAML types-tzlocal - name: Check python compatibility for ${{ matrix.os }}/${{ matrix.version }} run: mypy ./exegol.py --ignore-missing-imports --check-untyped-defs --python-version ${{ matrix.version }} --platform ${{ matrix.os }} diff --git a/exegol-resources b/exegol-resources index 833f0359..314c14d8 160000 --- a/exegol-resources +++ b/exegol-resources @@ -1 +1 @@ -Subproject commit 833f035933eec193fc8a32cc31c44eee80564a6b +Subproject commit 314c14d8e275f6d9111b7f434b3f846444fdbf60 diff --git a/exegol/config/ConstantConfig.py b/exegol/config/ConstantConfig.py index 5bd0e4c0..c4519dde 100644 --- a/exegol/config/ConstantConfig.py +++ b/exegol/config/ConstantConfig.py @@ -5,7 +5,7 @@ class ConstantConfig: """Constant parameters information""" # Exegol Version - version: str = "4.3.4" + version: str = "4.3.5" # Exegol documentation link documentation: str = "https://exegol.rtfd.io/" diff --git a/exegol/console/TUI.py b/exegol/console/TUI.py index 893d4cbe..0364bc73 100644 --- a/exegol/console/TUI.py +++ b/exegol/console/TUI.py @@ -7,6 +7,7 @@ from rich.prompt import Prompt from rich.table import Table +from exegol.config.EnvInfo import EnvInfo from exegol.console import ConsoleFormat from exegol.console.ConsoleFormat import boolFormatter, getColor, richLen from exegol.console.ExegolProgress import ExegolProgress @@ -17,7 +18,6 @@ from exegol.model.ExegolContainerTemplate import ExegolContainerTemplate from exegol.model.ExegolImage import ExegolImage from exegol.model.SelectableInterface import SelectableInterface -from exegol.config.EnvInfo import EnvInfo from exegol.utils.ExeLog import logger, console, ExeLog @@ -437,7 +437,10 @@ def __buildContainerRecapTable(container: ExegolContainerTemplate): recap.title = "[not italic]:white_medium_star: [/not italic][gold3][g]Container summary[/g][/gold3]" # Header recap.add_column(f"[bold blue]Name[/bold blue]{os.linesep}[bold blue]Image[/bold blue]", justify="right") - container_info_header = f"{container.getDisplayName()}{os.linesep}{container.image.getName()}" + container_status = container.getTextStatus() + + container_info_header = (f"{container.getDisplayName()} {'(' + container_status + ')' if container_status else ''}{os.linesep}" + f"{container.image.getName()}") if "N/A" not in container.image.getImageVersion(): container_info_header += f" - v.{container.image.getImageVersion()}" if "Unknown" not in container.image.getStatus(): diff --git a/exegol/console/cli/actions/ExegolParameters.py b/exegol/console/cli/actions/ExegolParameters.py index cbe0addb..2c8e10bb 100644 --- a/exegol/console/cli/actions/ExegolParameters.py +++ b/exegol/console/cli/actions/ExegolParameters.py @@ -289,4 +289,4 @@ class Version(Command): """Print current Exegol version""" def __call__(self, *args, **kwargs): - return ExegolManager.print_version + return lambda: None diff --git a/exegol/manager/ExegolController.py b/exegol/manager/ExegolController.py index ad36116a..3c90ca4f 100644 --- a/exegol/manager/ExegolController.py +++ b/exegol/manager/ExegolController.py @@ -1,3 +1,5 @@ +import logging + try: import docker import requests @@ -69,8 +71,16 @@ def main(): logger.info("Exiting") except git.exc.GitCommandError as git_error: print_exception_banner() + # Printing git stderr as raw to avoid any Rich parsing error + logger.debug("Full git output:") + logger.raw(git_error, level=logging.DEBUG) + logger.empty_line() error = git_error.stderr.strip().split(": ")[-1].strip("'") - logger.critical(f"A critical error occurred while running this git command: {' '.join(git_error.command)} => {error}") + logger.error("Git error received:") + # Printing git error as raw to avoid any Rich parsing error + logger.raw(error, level=logging.ERROR) + logger.empty_line() + logger.critical(f"A critical error occurred while running this git command: {' '.join(git_error.command)}") except Exception: print_exception_banner() console.print_exception(show_locals=True, suppress=[docker, requests, git]) diff --git a/exegol/manager/ExegolManager.py b/exegol/manager/ExegolManager.py index ab93d90f..627f8cc9 100644 --- a/exegol/manager/ExegolManager.py +++ b/exegol/manager/ExegolManager.py @@ -362,7 +362,13 @@ def __loadOrCreateContainer(cls, # Return cache return cls.__container container_tag: Optional[str] = override_container if override_container is not None else ParametersManager().containertag - container_tags: Optional[Sequence[str]] = ParametersManager().multicontainertag + container_tags: Optional[List[str]] = None + if ParametersManager().multicontainertag: + container_tags = [] + for tag in ParametersManager().multicontainertag: + # Prevent duplicate tag selection + if tag not in container_tags: + container_tags.append(tag) try: if container_tag is None and (container_tags is None or len(container_tags) == 0): # Interactive container selection @@ -438,51 +444,55 @@ def __interactiveSelection(cls, @classmethod def __prepareContainerConfig(cls): """Create Exegol configuration with user input""" - # Create default exegol config - config = ContainerConfig() - # Container configuration from user CLI options - if ParametersManager().X11: - config.enableGUI() - if ParametersManager().share_timezone: - config.enableSharedTimezone() - config.setNetworkMode(ParametersManager().host_network) - if ParametersManager().ports is not None: - for port in ParametersManager().ports: - config.addRawPort(port) - if ParametersManager().my_resources: - config.enableMyResources() - if ParametersManager().exegol_resources: - config.enableExegolResources() - if ParametersManager().log: - config.enableShellLogging(ParametersManager().log_method, - UserConfig().shell_logging_compress ^ ParametersManager().log_compress) - if ParametersManager().workspace_path: - if ParametersManager().mount_current_dir: - logger.warning(f'Workspace conflict detected (-cwd cannot be use with -w). Using: {ParametersManager().workspace_path}') - config.setWorkspaceShare(ParametersManager().workspace_path) - elif ParametersManager().mount_current_dir: - config.enableCwdShare() - if ParametersManager().privileged: - config.setPrivileged() - elif ParametersManager().capabilities is not None: - for cap in ParametersManager().capabilities: - config.addCapability(cap) - if ParametersManager().volumes is not None: - for volume in ParametersManager().volumes: - config.addRawVolume(volume) - if ParametersManager().devices is not None: - for device in ParametersManager().devices: - config.addUserDevice(device) - if ParametersManager().vpn is not None: - config.enableVPN() - if ParametersManager().envs is not None: - for env in ParametersManager().envs: - config.addRawEnv(env) - if UserConfig().desktop_default_enable ^ ParametersManager().desktop: - config.enableDesktop(ParametersManager().desktop_config) - if ParametersManager().comment: - config.addComment(ParametersManager().comment) - return config + try: + # Create default exegol config + config = ContainerConfig() + # Container configuration from user CLI options + if ParametersManager().X11: + config.enableGUI() + if ParametersManager().share_timezone: + config.enableSharedTimezone() + config.setNetworkMode(ParametersManager().host_network) + if ParametersManager().ports is not None: + for port in ParametersManager().ports: + config.addRawPort(port) + if ParametersManager().my_resources: + config.enableMyResources() + if ParametersManager().exegol_resources: + config.enableExegolResources() + if ParametersManager().log: + config.enableShellLogging(ParametersManager().log_method, + UserConfig().shell_logging_compress ^ ParametersManager().log_compress) + if ParametersManager().workspace_path: + if ParametersManager().mount_current_dir: + logger.warning(f'Workspace conflict detected (-cwd cannot be use with -w). Using: {ParametersManager().workspace_path}') + config.setWorkspaceShare(ParametersManager().workspace_path) + elif ParametersManager().mount_current_dir: + config.enableCwdShare() + if ParametersManager().privileged: + config.setPrivileged() + elif ParametersManager().capabilities is not None: + for cap in ParametersManager().capabilities: + config.addCapability(cap) + if ParametersManager().volumes is not None: + for volume in ParametersManager().volumes: + config.addRawVolume(volume) + if ParametersManager().devices is not None: + for device in ParametersManager().devices: + config.addUserDevice(device) + if ParametersManager().vpn is not None: + config.enableVPN() + if ParametersManager().envs is not None: + for env in ParametersManager().envs: + config.addRawEnv(env) + if UserConfig().desktop_default_enable ^ ParametersManager().desktop: + config.enableDesktop(ParametersManager().desktop_config) + if ParametersManager().comment: + config.addComment(ParametersManager().comment) + return config + except CancelOperation as e: + logger.critical(f"Unable to create a new container: {e}") + raise e @classmethod def __createContainer(cls, name: Optional[str]) -> ExegolContainer: diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 9a2c672e..c34ba06b 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -24,8 +24,12 @@ from exegol.model.ExegolModules import ExegolModules from exegol.utils import FsUtils from exegol.utils.ExeLog import logger, ExeLog +from exegol.utils.FsUtils import check_sysctl_value from exegol.utils.GuiUtils import GuiUtils +if EnvInfo.is_windows_shell or EnvInfo.is_mac_shell: + from tzlocal import get_localzone_name + class ContainerConfig: """Configuration class of an exegol container""" @@ -38,6 +42,12 @@ class ContainerConfig: __static_gui_envs = {"_JAVA_AWT_WM_NONREPARENTING": "1", "QT_X11_NO_MITSHM": "1"} __default_desktop_port = {"http": 6080, "vnc": 5900} + # Verbose only filters + __verbose_only_envs = ["DISPLAY", "WAYLAND_DISPLAY", "XDG_SESSION_TYPE", "XDG_RUNTIME_DIR", "PATH", "TZ", "_JAVA_OPTIONS"] + __verbose_only_mounts = ['/tmp/.X11-unix', '/opt/resources', '/etc/localtime', + '/etc/timezone', '/my-resources', '/opt/my-resources', + '/.exegol/entrypoint.sh', '/.exegol/spawn.sh', '/tmp/wayland-0', '/tmp/wayland-1'] + # Whitelist device for Docker Desktop __whitelist_dd_devices = ["/dev/net/tun"] @@ -126,6 +136,9 @@ def __parseContainerConfig(self, container: Container): """Parse Docker object to setup self configuration""" # Reset default attributes self.__passwd = None + self.__share_timezone = False + self.__my_resources = False + self.__enable_gui = False # Container Config section container_config = container.attrs.get("Config", {}) self.tty = container_config.get("Tty", True) @@ -133,14 +146,6 @@ def __parseContainerConfig(self, container: Container): self.__parseLabels(container_config.get("Labels", {})) self.interactive = container_config.get("OpenStdin", True) self.legacy_entrypoint = container_config.get("Entrypoint") is None - self.__enable_gui = False - 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", {}) @@ -158,8 +163,6 @@ def __parseContainerConfig(self, container: Container): logger.debug(f"└── Load devices : {self.__devices}") # Volumes section - self.__share_timezone = False - self.__my_resources = False self.__parseMounts(container.attrs.get("Mounts", []), container.name.replace('exegol-', '')) # Network section @@ -173,6 +176,15 @@ def __parseEnvs(self, envs: List[str]): logger.debug(f"└── Parsing envs : {env}") # Removing " and ' at the beginning and the end of the string before splitting key / value self.addRawEnv(env.strip("'").strip('"')) + 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") + if "TZ" in envs_key: + self.__share_timezone = True def __parseLabels(self, labels: Dict[str, str]): """Parse envs object syntax""" @@ -401,6 +413,11 @@ def enableGUI(self): # TODO support pulseaudio for k, v in self.__static_gui_envs.items(): self.addEnv(k, v) + + # Fix XQuartz render: https://github.com/ThePorgs/Exegol/issues/229 + if EnvInfo.isMacHost(): + self.addEnv("_JAVA_OPTIONS", '-Dsun.java2d.xrender=false') + self.__enable_gui = True def __disableGUI(self): @@ -419,37 +436,37 @@ def __disableGUI(self): def enableSharedTimezone(self): """Procedure to enable shared timezone feature""" - if EnvInfo.is_windows_shell: - logger.warning("Timezone sharing is not supported from a Windows shell. Skipping.") - return - elif EnvInfo.isMacHost(): - # On Orbstack /etc cannot be shared + we should test how Orbstack handle symlink - # With docker desktop, symlink are resolved as full path on container creation. When tzdata is updated on the host, the container can no longer be started because the files of the previous package version are missing. - # TODO Test if env var can be used as replacement - logger.warning("Timezone sharing on Mac is not supported (for stability reasons). Skipping.") - return if not self.__share_timezone: logger.verbose("Config: Enabling host timezones") - # Try to share /etc/timezone (deprecated old timezone file) - try: - self.addVolume("/etc/timezone", "/etc/timezone", read_only=True, must_exist=True) - logger.verbose("Volume was successfully added for [magenta]/etc/timezone[/magenta]") - timezone_loaded = True - except CancelOperation: - logger.verbose("File /etc/timezone is missing on host, cannot create volume for this.") - timezone_loaded = False - # Try to share /etc/localtime (new timezone file) - try: - self.addVolume("/etc/localtime", "/etc/localtime", read_only=True, must_exist=True) - logger.verbose("Volume was successfully added for [magenta]/etc/localtime[/magenta]") - except CancelOperation as e: - if not timezone_loaded: - # If neither file was found, disable the functionality - logger.error(f"The host's timezone could not be shared: {e}") - return + if EnvInfo.is_windows_shell or EnvInfo.is_mac_shell: + current_tz = get_localzone_name() + if current_tz: + logger.debug(f"Sharing timezone via TZ env var: '{current_tz}'") + self.addEnv("TZ", current_tz) else: - logger.warning("File [magenta]/etc/localtime[/magenta] is [orange3]missing[/orange3] on host, " - "cannot create volume for this. Relying instead on [magenta]/etc/timezone[/magenta] [orange3](deprecated)[/orange3].") + logger.warning("Your system timezone cannot be shared.") + return + else: + # Try to share /etc/timezone (deprecated old timezone file) + try: + self.addVolume("/etc/timezone", "/etc/timezone", read_only=True, must_exist=True) + logger.verbose("Volume was successfully added for [magenta]/etc/timezone[/magenta]") + timezone_loaded = True + except CancelOperation: + logger.verbose("File /etc/timezone is missing on host, cannot create volume for this.") + timezone_loaded = False + # Try to share /etc/localtime (new timezone file) + try: + self.addVolume("/etc/localtime", "/etc/localtime", read_only=True, must_exist=True) + logger.verbose("Volume was successfully added for [magenta]/etc/localtime[/magenta]") + except CancelOperation as e: + if not timezone_loaded: + # If neither file was found, disable the functionality + logger.error(f"The host's timezone could not be shared: {e}") + return + else: + logger.warning("File [magenta]/etc/localtime[/magenta] is [orange3]missing[/orange3] on host, " + "cannot create volume for this. Relying instead on [magenta]/etc/timezone[/magenta] [orange3](deprecated)[/orange3].") self.__share_timezone = True def __disableSharedTimezone(self): @@ -627,9 +644,8 @@ def enableVPN(self, config_path: Optional[str] = None): skip_sysctl = False if self.__network_host and EnvInfo.is_linux_shell: # Check if IPv6 have been disabled on the host with sysctl - with open('/proc/sys/net/ipv6/conf/all/disable_ipv6', 'r') as conf: - if int(conf.read()) == 0: - skip_sysctl = True + if check_sysctl_value("net.ipv6.conf.all.disable_ipv6", "0"): + skip_sysctl = True if not skip_sysctl: self.__addSysctl("net.ipv6.conf.all.disable_ipv6", "0") # Add tun device, this device is needed to create VPN tunnels @@ -872,17 +888,18 @@ def __removeCapability(self, cap_string: str): # When the capability is not present return False - def __addSysctl(self, sysctl_key: str, config: str): + def __addSysctl(self, sysctl_key: str, config: Union[str, int]): """Add a linux sysctl to the container""" if sysctl_key in self.__sysctls.keys(): logger.warning(f"Sysctl {sysctl_key} already setup to '{self.__sysctls[sysctl_key]}'. Skipping.") return - if self.__network_host: + # Docs of supported sysctl by linux / docker: https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls + if self.__network_host and sysctl_key.startswith('net.'): logger.warning(f"The sysctl container configuration is [red]not[/red] supported by docker in [blue]host[/blue] network mode.") logger.warning(f"Skipping the sysctl config: [magenta]{sysctl_key}[/magenta] = [orange3]{config}[/orange3].") logger.warning(f"If this configuration is mandatory in your situation, try to change it in sudo mode on your host.") return - self.__sysctls[sysctl_key] = config + self.__sysctls[sysctl_key] = str(config) def __removeSysctl(self, sysctl_key: str): """Remove a linux capability from the container's config""" @@ -992,28 +1009,25 @@ def addVolume(self, # Docker Desktop for Windows based on WSL2 don't have filesystem limitation if EnvInfo.isMacHost(): # Add support for /etc - # TODO check if path_match + replace really useful , path_match rever used - path_match = host_path - if path_match.startswith("/opt/") and EnvInfo.isOrbstack(): - msg = f"{EnvInfo.getDockerEngine().value} cannot mount directory from [magenta]/opt/[/magenta] host path." - if path_match.endswith("entrypoint.sh") or path_match.endswith("spawn.sh"): + if host_path.startswith("/opt/") and EnvInfo.isOrbstack(): + msg = f"{EnvInfo.getDockerEngine().value} cannot mount directory from /opt/ host path." + if host_path.endswith("entrypoint.sh") or host_path.endswith("spawn.sh"): msg += " Your exegol installation cannot be stored under this directory." logger.critical(msg) + else: + msg += f" The volume {host_path} cannot be mounted to the container, please move it outside of this directory." raise CancelOperation(msg) - if path_match.startswith("/etc/"): - if EnvInfo.isOrbstack(): - raise CancelOperation(f"{EnvInfo.getDockerEngine().value} doesn't support sharing [magenta]/etc[/magenta] files with the container") - path_match = path_match.replace("/etc/", "/private/etc/") if EnvInfo.isDockerDesktop(): match = False # Find a match for resource in EnvInfo.getDockerDesktopResources(): - if path_match.startswith(resource): + if host_path.startswith(resource): match = True break if not match: logger.error(f"Bind volume from {host_path} is not possible, Docker Desktop configuration is [red]incorrect[/red].") - logger.critical(f"You need to modify the [green]Docker Desktop[/green] config and [green]add[/green] this path (or the root directory) in [magenta]Docker Desktop > Preferences > Resources > File Sharing[/magenta] configuration.") + logger.critical(f"You need to modify the [green]Docker Desktop[/green] config and [green]add[/green] this path (or the root directory) in " + f"[magenta]Docker Desktop > Preferences > Resources > File Sharing[/magenta] configuration.") # Choose to update fs directory perms if available and depending on user choice # 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)) @@ -1268,9 +1282,13 @@ def addRawVolume(self, volume_string): def addUserDevice(self, user_device_config: str): """Add a device from a user parameters""" - if EnvInfo.isDockerDesktop() and user_device_config not in self.__whitelist_dd_devices: - logger.warning("Docker desktop (Windows & macOS) does not support USB device passthrough.") - logger.verbose("Official doc: https://docs.docker.com/desktop/faqs/#can-i-pass-through-a-usb-device-to-a-container") + if (EnvInfo.isDockerDesktop() or EnvInfo.isOrbstack()) and user_device_config not in self.__whitelist_dd_devices: + if EnvInfo.isDockerDesktop(): + logger.warning("Docker desktop (Windows & macOS) does not support USB device passthrough.") + logger.verbose("Official doc: https://docs.docker.com/desktop/faqs/#can-i-pass-through-a-usb-device-to-a-container") + elif EnvInfo.isOrbstack(): + logger.warning("Orbstack does not support (yet) USB device passthrough.") + logger.verbose("Official doc: https://docs.orbstack.dev/machines/#usb-devices") logger.critical("Device configuration cannot be applied, aborting operation.") self.__addDevice(user_device_config) @@ -1381,12 +1399,9 @@ def getTextCreationDate(self) -> str: def getTextMounts(self, verbose: bool = False) -> str: """Text formatter for Mounts configurations. The verbose mode does not exclude technical volumes.""" result = '' - hidden_mounts = ['/tmp/.X11-unix', '/opt/resources', '/etc/localtime', - '/etc/timezone', '/my-resources', '/opt/my-resources', - '/.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: + if not verbose and mount.get('Target') in self.__verbose_only_mounts: continue read_only_text = f"[bright_black](RO)[/bright_black] " if verbose else '' read_write_text = f"[orange3](RW)[/orange3] " if verbose else '' @@ -1412,7 +1427,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", "WAYLAND_DISPLAY", "XDG_SESSION_TYPE", "XDG_RUNTIME_DIR", "PATH"]: + if not verbose and k in list(self.__static_gui_envs.keys()) + [v.value for v in self.ExegolEnv] + self.__verbose_only_envs: continue result += f"{k}={v}{os.linesep}" return result diff --git a/exegol/model/ExegolContainer.py b/exegol/model/ExegolContainer.py index c3238278..e49f4800 100644 --- a/exegol/model/ExegolContainer.py +++ b/exegol/model/ExegolContainer.py @@ -77,10 +77,10 @@ def getTextStatus(self) -> str: if status == "unknown": return "Unknown" elif status == "exited": - return "[red]Stopped" + return "[red]Stopped[/red]" elif status == "running": - return "[green]Running" - return status + return "[green]Running[/green]" + return f"[orange3]{status}[/orange3]" def isNew(self) -> bool: """Check if the container has just been created or not""" @@ -341,9 +341,9 @@ def __applyXhostACL(self): with console.status(f"Starting XQuartz...", spinner_style="blue"): os.system(f"xhost + localhost > /dev/null") else: - logger.debug(f"Adding xhost ACL to local:{self.config.hostname}") + logger.debug(f"Adding xhost ACL to local:{self.config.getUsername()}") # add linux local ACL - os.system(f"xhost +local:{self.config.hostname} > /dev/null") + os.system(f"xhost +local:{self.config.getUsername()} > /dev/null") def __updatePasswd(self): """ diff --git a/exegol/model/ExegolContainerTemplate.py b/exegol/model/ExegolContainerTemplate.py index b0f3b678..2b4b9fed 100644 --- a/exegol/model/ExegolContainerTemplate.py +++ b/exegol/model/ExegolContainerTemplate.py @@ -46,3 +46,6 @@ def getDisplayName(self) -> str: if self.container_name != self.config.hostname: return f"{self.name} [bright_black]({self.config.hostname})[/bright_black]" return self.name + + def getTextStatus(self) -> str: + return "" diff --git a/exegol/utils/FsUtils.py b/exegol/utils/FsUtils.py index 2e1267c7..5d81f58e 100644 --- a/exegol/utils/FsUtils.py +++ b/exegol/utils/FsUtils.py @@ -90,3 +90,17 @@ def setGidPermission(root_folder: Path): logger.raw(f"sudo chgrp -R $(id -g) {root_folder} && sudo find {root_folder} -type d -exec chmod g+rws {{}} \\;", level=logging.WARNING) logger.empty_line() logger.empty_line() + + +def check_sysctl_value(sysctl: str, compare_to: str) -> bool: + sysctl_path = "/proc/sys/" + sysctl.replace('.', '/') + try: + with open(sysctl_path, 'r') as conf: + config = conf.read().strip() + logger.debug(f"Checking sysctl value {sysctl}={config} (compare to {compare_to})") + return conf.read().strip() == compare_to + except FileNotFoundError: + logger.debug(f"Sysctl file {sysctl} not found!") + except PermissionError: + logger.debug(f"Unable to read sysctl {sysctl} permission!") + return False diff --git a/exegol/utils/GuiUtils.py b/exegol/utils/GuiUtils.py index 607a912e..47ccc741 100644 --- a/exegol/utils/GuiUtils.py +++ b/exegol/utils/GuiUtils.py @@ -41,10 +41,9 @@ def isWaylandGuiAvailable(cls) -> bool: :return: bool """ if EnvInfo.isWindowsHost(): - return False # TODO To Be defined (WSLg works fine for now) - # elif EnvInfo.isMacHost(): - # return False - # Linux or Mac, rely on var env settings + return False + elif EnvInfo.isMacHost(): + return False return EnvInfo.isWaylandAvailable() @classmethod @@ -211,16 +210,20 @@ def __windowsGuiChecks(cls) -> bool: logger.debug("Testing WSLg availability") # WSL + WSLg must be available on the Windows host for the GUI to work through X11 sharing if not cls.__wsl_available(): + if sys.platform != "win32" and os.getuid() == 0: + logger.critical("You are running exegol as [red]root[/red]! The root user cannot be used to run Exegol on a Windows environment.") logger.error("WSL is [orange3]not available[/orange3] on your system. X11 sharing is not supported.") return False + logger.debug("WSL is [green]available[/green] on the local system") # Only WSL2 support WSLg if EnvInfo.getDockerEngine() != EnvInfo.DockerEngine.WLS2: + logger.debug(f"Docker current engine: {EnvInfo.getDockerEngine().value}") logger.error("Docker must be run with [orange3]WSL2[/orange3] engine in order to support X11 sharing (i.e. GUI apps).") return False - logger.debug("WSL is [green]available[/green] and docker is using WSL2") + logger.debug("Docker is using [green]WSL2[/green]") # X11 socket can only be shared from a WSL (to find WSLg mount point) if EnvInfo.current_platform != "WSL": - logger.debug("Exegol is running from a Windows context (e.g. Powershell), a WSL instance must be found to share WSLg X11 socket") + logger.debug("Exegol is running from a Windows context (e.g. Powershell), a WSL instance must be found to share the WSLg X11 socket") cls.__distro_name = cls.__find_wsl_distro() logger.debug(f"Set WSL Distro as: '{cls.__distro_name}'") # If no WSL is found, propose to continue without GUI (X11 sharing) @@ -252,12 +255,16 @@ def __wsl_test(path, name: Optional[str] = "docker-desktop") -> bool: if EnvInfo.isWindowsHost(): wsl = shutil.which("wsl.exe") if not wsl: + logger.warning("wsl.exe seems to be unavailable on your system.") return False if name is None: + logger.debug(f"Running: wsl.exe test -f {path}") ret = subprocess.run(["wsl.exe", "test", "-f", path]) else: + logger.debug(f"Running: wsl.exe test -d {name} -f {path}") ret = subprocess.run(["wsl.exe", "-d", name, "test", "-f", path]) return ret.returncode == 0 + logger.debug("Trying to run a WSL test without Windows?") return False @classmethod @@ -282,11 +289,17 @@ def __wsl_available(cls) -> bool: if EnvInfo.isWindowsHost(): wsl = shutil.which("wsl.exe") if not wsl: + logger.debug("wsl.exe not found on the local system.") return False + logger.debug("running: wsl.exe --status") ret = subprocess.Popen(["wsl.exe", "--status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) ret.wait() if ret.returncode == 0: return True + else: + logger.debug(f"wsl.exe --status return code {ret.returncode}") + logger.debug(str(ret.stdout)) + logger.debug(str(ret.stderr)) logger.debug("WSL status command failed.. Trying a fallback check method.") return cls.__wsl_test("/etc/os-release", name=None) or cls.__wsl_test("/etc/os-release") @@ -300,10 +313,12 @@ def __wslg_installed(cls) -> bool: if (Path("/mnt/host/wslg/versions.txt").is_file() or Path("/mnt/wslg/versions.txt").is_file()): return True + logger.debug("Unable to find WSLg locally.. Check /mnt/wslg/ or /mnt/host/wslg/") else: if (cls.__wsl_test("/mnt/host/wslg/versions.txt", name=cls.__distro_name) or cls.__wsl_test("/mnt/wslg/versions.txt", name=cls.__distro_name)): return True + logger.debug(f"Unable to find WSLg.. Check /mnt/wslg/ or /mnt/host/wslg/ on {cls.__distro_name}") logger.debug("WSLg check failed.. Trying a fallback check method.") return cls.__wsl_test("/mnt/host/wslg/versions.txt") or cls.__wsl_test("/mnt/wslg/versions.txt", name=None) @@ -318,14 +333,15 @@ def __wslg_eligible() -> bool: return True try: os_version_raw, _, build_number_raw = EnvInfo.getWindowsRelease().split('.')[:3] + os_version = int(os_version_raw) except ValueError: logger.debug(f"Impossible to find the version of windows: '{EnvInfo.getWindowsRelease()}'") logger.error("Exegol can't know if your [orange3]version of Windows[/orange3] can support dockerized GUIs (X11 sharing).") return False # Available for Windows 10 & 11 - os_version = int(os_version_raw) if os_version >= 10: return True + logger.debug(f"Current version of Windows doesn't support WSLg: {os_version_raw}.?.{build_number_raw}") return False @classmethod @@ -333,6 +349,7 @@ def __find_wsl_distro(cls) -> str: distro_name = "" # these distros cannot be used to load WSLg socket blacklisted_distro = ["docker-desktop", "docker-desktop-data"] + logger.debug("Running: C:\\Windows\\system32\\wsl.exe -l") ret = subprocess.Popen(["C:\\Windows\\system32\\wsl.exe", "-l"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Wait for WSL process to end ret.wait() @@ -387,6 +404,7 @@ def __find_wsl_distro(cls) -> str: @classmethod def __create_default_wsl(cls) -> bool: logger.info("Creating Ubuntu WSL distribution. Please wait.") + logger.debug("Running: C:\\Windows\\system32\\wsl.exe --install -d Ubuntu") ret = subprocess.Popen(["C:\\Windows\\system32\\wsl.exe", "--install", "-d", "Ubuntu"], stderr=subprocess.PIPE) ret.wait() logger.info("Please follow installation instructions on the new window.") @@ -402,6 +420,7 @@ def __create_default_wsl(cls) -> bool: docker_settings = EnvInfo.getDockerDesktopSettings() if docker_settings is not None and docker_settings.get("enableIntegrationWithDefaultWslDistro", False): logger.verbose("Set WSL Ubuntu as default to automatically enable docker integration") + logger.debug("Running: C:\\Windows\\system32\\wsl.exe -s Ubuntu") # Set new WSL distribution as default to start it and enable docker integration ret = subprocess.Popen(["C:\\Windows\\system32\\wsl.exe", "-s", "Ubuntu"], stderr=subprocess.PIPE) ret.wait() diff --git a/exegol/utils/WebUtils.py b/exegol/utils/WebUtils.py index c3da213d..d6da2580 100644 --- a/exegol/utils/WebUtils.py +++ b/exegol/utils/WebUtils.py @@ -132,6 +132,9 @@ def __runRequest(cls, url: str, service_name: str, headers: Optional[Dict] = Non https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy') if https_proxy: proxies['https'] = https_proxy + no_proxy = os.environ.get('NO_PROXY') or os.environ.get('no_proxy') + if no_proxy: + proxies['no_proxy'] = no_proxy response = requests.request(method=method, url=url, timeout=(10, 20), verify=ParametersManager().verify, headers=headers, data=data, proxies=proxies if len(proxies) > 0 else None) return response except requests.exceptions.HTTPError as e: diff --git a/exegol/utils/argParse.py b/exegol/utils/argParse.py index ca3aa63a..31a5e900 100644 --- a/exegol/utils/argParse.py +++ b/exegol/utils/argParse.py @@ -1,8 +1,9 @@ import argparse -import argcomplete from logging import CRITICAL from typing import IO, Optional, List, Union, Dict, cast +import argcomplete + from exegol.console.cli.actions.Command import Command, Option from exegol.utils.ExeLog import logger @@ -19,8 +20,9 @@ def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None: class Parser: """Custom Exegol CLI Parser. Main controller of argument building and parsing.""" - __description = "This Python script is a wrapper for Exegol. It can be used to easily manage Exegol on " \ - "your machine." + __description = """This Python script is a wrapper for Exegol. It can be used to easily manage Exegol on your machine. + +[bold magenta]Exegol documentation:[/bold magenta] [underline magenta]https://exegol.rtfd.io[/underline magenta]""" __formatter_class = argparse.RawTextHelpFormatter def __init__(self, actions: List[Command]): @@ -53,8 +55,11 @@ def __set_action_parser(self) -> None: # Each action has a dedicated sub-parser with different options # the 'help' description of the current action is retrieved # from the comment of the corresponding action class + if action.__doc__ is None: + action.__doc__ = "Unknown action" sub_parser = self.subParser.add_parser(action.name, help=action.__doc__, - description=action.__doc__, + description=action.__doc__ + f"""\n +[bold magenta]Exegol documentation:[/bold magenta] [underline magenta]https://exegol.rtfd.io/en/latest/exegol-wrapper/{action.name}.html[/underline magenta]""", epilog=action.formatEpilog(), formatter_class=self.__formatter_class) sub_parser.set_defaults(action=action) diff --git a/exegol/utils/imgsync/entrypoint.sh b/exegol/utils/imgsync/entrypoint.sh index d5a2262b..cc700c38 100755 --- a/exegol/utils/imgsync/entrypoint.sh +++ b/exegol/utils/imgsync/entrypoint.sh @@ -93,6 +93,7 @@ function desktop() { if command -v desktop-start &> /dev/null then echo "Starting Exegol [green]desktop[/green] with [blue]${EXEGOL_DESKTOP_PROTO}[/blue]" + ln -sf /root/.vnc /var/log/exegol/desktop desktop-start &>> ~/.vnc/startup.log # Disable logging sleep 2 # Waiting 2 seconds for the Desktop to start before continuing else diff --git a/requirements.txt b/requirements.txt index b18d4f68..af0af7db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -docker~=7.0.0 -# Request holdback: temp fix for https://github.com/docker/docker-py/issues/3256 -requests~=2.31.0 +docker~=7.1.0 +requests~=2.32.3 rich~=13.7.1 GitPython~=3.1.43 -PyYAML>=6.0.1 -argcomplete~=3.3.0 \ No newline at end of file +PyYAML>=6.0.2 +argcomplete~=3.5.0 +tzlocal~=5.2 diff --git a/setup.py b/setup.py index 07dec25f..89d4c500 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,9 @@ for k, v in data_files_dict.items(): data_files.append((k, v)) +with open("requirements.txt", "r", encoding="utf-8") as f: + requirements = [x.strip() for x in f.readlines()] + setup( name='Exegol', version=__version__, @@ -54,14 +57,7 @@ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ], - install_requires=[ - 'docker~=7.0.0', - 'requests~=2.31.0', - 'rich~=13.7.1', - 'PyYAML', - 'GitPython~=3.1.43', - 'argcomplete~=3.3.0' - ], + install_requires=requirements, packages=find_packages(exclude=["tests"]), include_package_data=True, data_files=data_files, diff --git a/tests/test_exegol.py b/tests/test_exegol.py index 564e78ef..2de37b16 100644 --- a/tests/test_exegol.py +++ b/tests/test_exegol.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == '4.3.4' + assert __version__ == '4.3.5'