Create named pipes before opening them
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 31 Oct 2025 14:57:07 +0000 (15:57 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 31 Oct 2025 14:57:07 +0000 (15:57 +0100)
music_assistant/providers/airplay/protocols/_protocol.py
music_assistant/providers/airplay/protocols/raop.py

index 1cfc95ff4340a9f0810ce511da6a6c631319925c..5ba420ef67ff4b3f3ce775516aa7629979e80681 100644 (file)
@@ -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
index 4a775fd90413f9c82dccfe864d15a7a4546e5d4b..1df585820bb6005f8ccfe43e01a6c9a1dda74191 100644 (file)
@@ -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",