From 87eca2d8a47fefdd7f181255f003592bee9fc647 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 31 Oct 2025 15:57:07 +0100 Subject: [PATCH] Create named pipes before opening them --- .../providers/airplay/protocols/_protocol.py | 26 +++++++++++++------ .../providers/airplay/protocols/raop.py | 8 ++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/music_assistant/providers/airplay/protocols/_protocol.py b/music_assistant/providers/airplay/protocols/_protocol.py index 1cfc95ff..5ba420ef 100644 --- a/music_assistant/providers/airplay/protocols/_protocol.py +++ b/music_assistant/providers/airplay/protocols/_protocol.py @@ -104,12 +104,27 @@ class AirPlayProtocol(ABC): """Finish pairing process with given PIN (if supported).""" raise NotImplementedError("Pairing not implemented for this protocol") - async def _open_pipes(self) -> None: - """Open both named pipes in non-blocking mode for async I/O.""" - # Create named pipes first if they don't exist + async def _create_pipes(self) -> None: + """Create named pipes (FIFOs) before starting CLI process. + + This must be called before starting the CLI binary so the FIFOs exist + when the CLI tries to open them for reading. + """ await asyncio.to_thread(self._create_named_pipe, self.audio_named_pipe) await asyncio.to_thread(self._create_named_pipe, self.commands_named_pipe) + self.player.logger.debug("Named pipes created for streaming session") + + def _create_named_pipe(self, pipe_path: str) -> None: + """Create a named pipe (FIFO) if it doesn't exist.""" + if not os.path.exists(pipe_path): + os.mkfifo(pipe_path) + async def _open_pipes(self) -> None: + """Open both named pipes in non-blocking mode for async I/O. + + This must be called AFTER the CLI process has started and opened the pipes + for reading. Otherwise opening with O_WRONLY | O_NONBLOCK will fail with ENXIO. + """ # Open audio pipe with buffer size optimization self._audio_pipe = AsyncNamedPipeWriter(self.audio_named_pipe, logger=self.player.logger) await self._audio_pipe.open(increase_buffer=True) @@ -122,11 +137,6 @@ class AirPlayProtocol(ABC): self.player.logger.debug("Named pipes opened in non-blocking mode for streaming session") - def _create_named_pipe(self, pipe_path: str) -> None: - """Create a named pipe (FIFO) if it doesn't exist.""" - if not os.path.exists(pipe_path): - os.mkfifo(pipe_path) - async def stop(self) -> None: """Stop playback and cleanup.""" # Send stop command before setting _stopped flag diff --git a/music_assistant/providers/airplay/protocols/raop.py b/music_assistant/providers/airplay/protocols/raop.py index 4a775fd9..1df58582 100644 --- a/music_assistant/providers/airplay/protocols/raop.py +++ b/music_assistant/providers/airplay/protocols/raop.py @@ -100,6 +100,10 @@ class RaopStream(AirPlayProtocol): # https://github.com/music-assistant/libraop # we use this intermediate binary to do the actual streaming because attempts to do # so using pure python (e.g. pyatv) were not successful due to the realtime nature + + # Create named pipes before starting CLI process + await self._create_pipes() + cliraop_args = [ cli_binary, "-ntpstart", @@ -152,6 +156,10 @@ class RaopStream(AirPlayProtocol): """Start pairing process for this protocol (if supported).""" assert self.player.discovery_info is not None # for type checker cli_binary = await get_cli_binary(self.player.protocol) + + # Create named pipes before starting CLI process + await self._create_pipes() + cliraop_args = [ cli_binary, "-pair", -- 2.34.1