diff --git a/exegol/console/cli/actions/GenericParameters.py b/exegol/console/cli/actions/GenericParameters.py index 23c300fd..ee0bd1b7 100644 --- a/exegol/console/cli/actions/GenericParameters.py +++ b/exegol/console/cli/actions/GenericParameters.py @@ -100,6 +100,11 @@ def __init__(self, groupArgs: List[GroupArg]): default=True, dest="X11", help="Disable display sharing to run GUI-based applications (default: [green]Enabled[/green])") + self.sound_sharing = Option("--sound", + action="store_true", + default=False, + dest="sound_sharing", + help="Enable sound sharing") self.my_resources = Option("--disable-my-resources", action="store_false", default=True, @@ -191,6 +196,7 @@ def __init__(self, groupArgs: List[GroupArg]): {"arg": self.capabilities, "required": False}, {"arg": self.privileged, "required": False}, {"arg": self.devices, "required": False}, + {"arg": self.sound_sharing, "required": False}, {"arg": self.X11, "required": False}, {"arg": self.my_resources, "required": False}, {"arg": self.exegol_resources, "required": False}, diff --git a/exegol/manager/ExegolManager.py b/exegol/manager/ExegolManager.py index 58786451..97679310 100644 --- a/exegol/manager/ExegolManager.py +++ b/exegol/manager/ExegolManager.py @@ -435,6 +435,8 @@ def __prepareContainerConfig(cls): # Container configuration from user CLI options if ParametersManager().X11: config.enableGUI() + if ParametersManager().sound_sharing: + config.enableSound() if ParametersManager().share_timezone: config.enableSharedTimezone() config.setNetworkMode(ParametersManager().host_network) diff --git a/exegol/model/ContainerConfig.py b/exegol/model/ContainerConfig.py index 7ff6eef0..8f9cde6b 100644 --- a/exegol/model/ContainerConfig.py +++ b/exegol/model/ContainerConfig.py @@ -18,6 +18,7 @@ from exegol.utils.EnvInfo import EnvInfo from exegol.utils.ExeLog import logger, ExeLog from exegol.utils.GuiUtils import GuiUtils +from exegol.utils.SoundUtils import SoundUtils from exegol.utils.UserConfig import UserConfig @@ -32,6 +33,7 @@ class ContainerConfig: # Reference static config data __static_gui_envs = {"_JAVA_AWT_WM_NONREPARENTING": "1", "QT_X11_NO_MITSHM": "1"} + __static_pulseaudio_envs = {"PULSE_SERVER": "unix:/run/user/0/pulse/native"} # Label features (wrapper method to enable the feature / label name) __label_features = {"enableShellLogging": "org.exegol.feature.shell_logging"} @@ -41,6 +43,7 @@ class ContainerConfig: def __init__(self, container: Optional[Container] = None): """Container config default value""" self.__enable_gui: bool = False + self.__enable_sound: bool = False self.__share_timezone: bool = False self.__my_resources: bool = False self.__my_resources_path: str = "/opt/my-resources" @@ -228,6 +231,16 @@ def interactiveConfig(self, container_name: str) -> List[str]: if not self.__enable_gui: command_options.append("--disable-X11") + # Sound sharing + if self.__enable_sound: + if Confirm("Do you want to [orange3]disable[/orange3] [blue]sound sharing[/blue]?", False): + self.__disableSound() + elif Confirm("Do you want to [green]enable[/green] [blue]sound sharing[/blue]?", False): + self.enableSound() + # Command builder info + if self.__enable_sound: + command_options.append("--sound") + # Timezone config if self.__share_timezone: if Confirm("Do you want to [orange3]remove[/orange3] your [blue]shared timezone[/blue] config?", False): @@ -296,6 +309,35 @@ def interactiveConfig(self, container_name: str) -> List[str]: return command_options + def enableSound(self): + """Procedure to enable sound feature""" + if not SoundUtils.isPulseAudioAvailable(): + logger.error("Sound sharing feature is [red]not available[/red] on your environment. [orange3]Skipping[/orange3].") + return + if not self.__enable_sound: + logger.verbose("Config: Enabling sound sharing") + try: + self.addVolume(SoundUtils.getPulseAudioSocketPath(), "/run/user/0/pulse/native", must_exist=False) + # fixme must_exist cannot be set to True since addVolume will fail, this needs to be fixed + self.addVolume(SoundUtils.getPulseAudioCookiePath(), "/root/.config/pulse/cookie", must_exist=True) + except CancelOperation as e: + logger.warning(f"Sound socket sharing could not be enabled: {e}") + return + for k, v in self.__static_pulseaudio_envs.items(): + self.addEnv(k, v) + self.__enable_sound = True + + + def __disableSound(self): + """Procedure to disable sound feature (Only for interactive config)""" + if self.__enable_sound: + self.__enable_sound = False + logger.verbose("Config: Disabling sound sharing") + self.removeVolume(container_path="/run/user/0/pulse/native") + self.removeVolume(container_path="/root/.config/pulse/cookie") + for k in self.__static_gui_envs.keys(): + self.removeEnv(k) + def enableGUI(self): """Procedure to enable GUI feature""" if not GuiUtils.isGuiAvailable(): @@ -308,7 +350,6 @@ def enableGUI(self): except CancelOperation as e: logger.warning(f"Graphical interface sharing could not be enabled: {e}") return - # TODO support pulseaudio self.addEnv("DISPLAY", GuiUtils.getDisplayEnv()) for k, v in self.__static_gui_envs.items(): self.addEnv(k, v) diff --git a/exegol/utils/SoundUtils.py b/exegol/utils/SoundUtils.py new file mode 100644 index 00000000..bfa0be45 --- /dev/null +++ b/exegol/utils/SoundUtils.py @@ -0,0 +1,47 @@ +import io +import os +from pathlib import Path +from typing import Optional + +from exegol.console.ExegolPrompt import Confirm +from exegol.exceptions.ExegolExceptions import CancelOperation +from exegol.utils.EnvInfo import EnvInfo +from exegol.utils.ExeLog import logger, console + + +class SoundUtils: + """This utility class allows determining if the current system supports the sound sharing + from the information of the system.""" + + @classmethod + def isPulseAudioAvailable(cls) -> bool: + """ + Check if the host OS has PulseAudio installed + :return: bool + """ + if "XDG_RUNTIME_DIR" in os.environ: + return Path(f"{os.getenv('XDG_RUNTIME_DIR')}/pulse/native").exists() + else: + return Path(f"/run/user/{os.getuid()}/pulse/native").exists() + + @classmethod + def getPulseAudioSocketPath(cls) -> str: + """ + Get the host path of the Pulse Audio socket + :return: + """ + # todo : find the path for windows/WSL + if "XDG_RUNTIME_DIR" in os.environ: + return f"{os.getenv('XDG_RUNTIME_DIR')}/pulse/native" + else: + return f"/run/user/{os.getuid()}/pulse/native" + + @classmethod + def getPulseAudioCookiePath(cls) -> str: + """ + Get the host path of the Pulse Audio cookie + :return: + """ + # todo : find the path for windows/WSL + # return f"{os.getenv('HOME')}/.config/pulse/cookie" + return str(Path().home() / ".config/pulse/cookie")