1- """Cross platform abstractions for inter-process communication
1+ """Cross- platform abstractions for inter-process communication
22
33On Unix, this uses AF_UNIX sockets.
44On Windows, this uses NamedPipes.
55"""
66
77from __future__ import annotations
88
9- import base64
10- import codecs
119import json
1210import os
1311import shutil
12+ import struct
1413import sys
1514import tempfile
1615from abc import abstractmethod
2019from typing import Final
2120from typing_extensions import Self
2221
22+ from librt .base64 import urlsafe_b64encode
2323from librt .internal import ReadBuffer , WriteBuffer
2424
2525if sys .platform == "win32" :
3737
3838 _IPCHandle = socket .socket
3939
40+ # Size of the message packed as !L, i.e. 4 bytes in network order (big-endian).
41+ HEADER_SIZE = 4
42+
4043
4144class IPCException (Exception ):
4245 """Exception for IPC issues."""
@@ -58,24 +61,29 @@ class IPCBase:
5861 def __init__ (self , name : str , timeout : float | None ) -> None :
5962 self .name = name
6063 self .timeout = timeout
64+ self .message_size : int | None = None
6165 self .buffer = bytearray ()
6266
63- def frame_from_buffer (self ) -> bytearray | None :
67+ def frame_from_buffer (self ) -> bytes | None :
6468 """Return a full frame from the bytes we have in the buffer."""
65- space_pos = self .buffer . find ( b" " )
66- if space_pos == - 1 :
69+ size = len ( self .buffer )
70+ if size < HEADER_SIZE :
6771 return None
68- # We have a full frame
69- bdata = self .buffer [:space_pos ]
70- self .buffer = self .buffer [space_pos + 1 :]
71- return bdata
72+ if self .message_size is None :
73+ self .message_size = struct .unpack ("!L" , self .buffer [:HEADER_SIZE ])[0 ]
74+ if size < self .message_size + HEADER_SIZE :
75+ return None
76+ # We have a full frame, avoid extra copy in case we get a large frame.
77+ bdata = memoryview (self .buffer )[HEADER_SIZE : HEADER_SIZE + self .message_size ]
78+ self .buffer = self .buffer [HEADER_SIZE + self .message_size :]
79+ self .message_size = None
80+ return bytes (bdata )
7281
7382 def read (self , size : int = 100000 ) -> str :
7483 return self .read_bytes (size ).decode ("utf-8" )
7584
7685 def read_bytes (self , size : int = 100000 ) -> bytes :
7786 """Read bytes from an IPC connection until we have a full frame."""
78- bdata : bytearray | None = bytearray ()
7987 if sys .platform == "win32" :
8088 while True :
8189 # Check if we already have a message in the buffer before
@@ -126,19 +134,19 @@ def read_bytes(self, size: int = 100000) -> bytes:
126134 self .buffer .extend (more )
127135
128136 if not bdata :
129- # Socket was empty and we didn't get any frame.
137+ # Socket was empty, and we didn't get any frame.
130138 # This should only happen if the socket was closed.
131139 return b""
132- return codecs . decode ( bdata , "base64" )
140+ return bdata
133141
134142 def write (self , data : str ) -> None :
135143 self .write_bytes (data .encode ("utf-8" ))
136144
137145 def write_bytes (self , data : bytes ) -> None :
138146 """Write to an IPC connection."""
139147
140- # Frame the data by urlencoding it and separating by space .
141- encoded_data = codecs . encode ( data , "base64" ) + b" "
148+ # Frame the data by adding fixed size header .
149+ encoded_data = struct . pack ( "!L" , len ( data )) + data
142150
143151 if sys .platform == "win32" :
144152 try :
@@ -226,9 +234,7 @@ class IPCServer(IPCBase):
226234
227235 def __init__ (self , name : str , timeout : float | None = None ) -> None :
228236 if sys .platform == "win32" :
229- name = r"\\.\pipe\{}-{}.pipe" .format (
230- name , base64 .urlsafe_b64encode (os .urandom (6 )).decode ()
231- )
237+ name = r"\\.\pipe\{}-{}.pipe" .format (name , urlsafe_b64encode (os .urandom (6 )).decode ())
232238 else :
233239 name = f"{ name } .sock"
234240 super ().__init__ (name , timeout )
0 commit comments