66import os
77import ssl
88import sys
9- from typing import Any , Generator
9+ import threading
10+ from typing import Any , Callable
1011
1112from .asyncio .client import ClientConnection , connect
12- from .asyncio .messages import SimpleQueue
1313from .exceptions import ConnectionClosed
1414from .frames import Close
15- from .streams import StreamReader
1615from .version import version as websockets_version
1716
1817
@@ -72,35 +71,6 @@ def print_over_input(string: str) -> None:
7271 sys .stdout .flush ()
7372
7473
75- class ReadLines (asyncio .Protocol ):
76- def __init__ (self ) -> None :
77- self .reader = StreamReader ()
78- self .messages : SimpleQueue [str ] = SimpleQueue ()
79-
80- def parse (self ) -> Generator [None , None , None ]:
81- while True :
82- sys .stdout .write ("> " )
83- sys .stdout .flush ()
84- line = yield from self .reader .read_line (sys .maxsize )
85- self .messages .put (line .decode ().rstrip ("\r \n " ))
86-
87- def connection_made (self , transport : asyncio .BaseTransport ) -> None :
88- self .parser = self .parse ()
89- next (self .parser )
90-
91- def data_received (self , data : bytes ) -> None :
92- self .reader .feed_data (data )
93- next (self .parser )
94-
95- def eof_received (self ) -> None :
96- self .reader .feed_eof ()
97- # next(self.parser) isn't useful and would raise EOFError.
98-
99- def connection_lost (self , exc : Exception | None ) -> None :
100- self .reader .discard ()
101- self .messages .abort ()
102-
103-
10474async def print_incoming_messages (websocket : ClientConnection ) -> None :
10575 async for message in websocket :
10676 if isinstance (message , str ):
@@ -109,15 +79,27 @@ async def print_incoming_messages(websocket: ClientConnection) -> None:
10979 print_during_input ("< (binary) " + message .hex ())
11080
11181
82+ def read_outgoing_messages (
83+ queue_for_sending : Callable [[str ], None ],
84+ notify_end_of_file : Callable [[], None ],
85+ ) -> None :
86+ while True :
87+ sys .stdout .write ("> " )
88+ sys .stdout .flush ()
89+ line = sys .stdin .readline ()
90+ if not line :
91+ notify_end_of_file ()
92+ break
93+ message = line .rstrip ("\r \n " )
94+ queue_for_sending (message )
95+
96+
11297async def send_outgoing_messages (
11398 websocket : ClientConnection ,
114- messages : SimpleQueue [str ],
99+ messages : asyncio . Queue [str ],
115100) -> None :
116101 while True :
117- try :
118- message = await messages .get ()
119- except EOFError :
120- break
102+ message = await messages .get ()
121103 try :
122104 await websocket .send (message )
123105 except ConnectionClosed : # pragma: no cover
@@ -133,17 +115,39 @@ async def interactive_client(uri: str, **kwargs: Any) -> None:
133115 else :
134116 print (f"Connected to { uri } ." )
135117
136- loop = asyncio .get_running_loop ()
137- transport , protocol = await loop .connect_read_pipe (ReadLines , sys .stdin )
138- incoming = asyncio .create_task (
139- print_incoming_messages (websocket ),
140- )
141- outgoing = asyncio .create_task (
142- send_outgoing_messages (websocket , protocol .messages ),
143- )
118+ # Read messsages from stdin in a thread because Windows doesn't support
119+ # reading asynchronously (#1681), and a daemon thread to avoid blocking
120+ # Ctrl-C because signals are only delivered to the main thread.
121+ loop = asyncio .get_event_loop ()
122+ messages : asyncio .Queue [str ] = asyncio .Queue ()
123+ # When dropping support for Python < 3.13, change notify_end_of_file() to
124+ # call messages.shutdown() and break when asyncio.QueueShutdownError is
125+ # raised in send_outgoing_messages().
126+ shutdown : asyncio .Future [None ] = loop .create_future ()
127+
128+ def queue_for_sending (message : str ) -> None :
129+ try :
130+ loop .call_soon_threadsafe (messages .put_nowait , message )
131+ except RuntimeError : # Event loop is closed # pragma: no cover
132+ pass
133+
134+ def notify_end_of_file () -> None :
135+ try :
136+ loop .call_soon_threadsafe (shutdown .set_result , None )
137+ except RuntimeError : # Event loop is closed # pragma: no cover
138+ pass
139+
140+ threading .Thread (
141+ target = read_outgoing_messages ,
142+ args = (queue_for_sending , notify_end_of_file ),
143+ daemon = True ,
144+ ).start ()
145+
146+ incoming = asyncio .create_task (print_incoming_messages (websocket ))
147+ outgoing = asyncio .create_task (send_outgoing_messages (websocket , messages ))
144148 try :
145149 await asyncio .wait (
146- [incoming , outgoing ],
150+ [incoming , outgoing , shutdown ],
147151 # Clean up and exit when the server closes the connection
148152 # or the user enters EOT (^D), whichever happens first.
149153 return_when = asyncio .FIRST_COMPLETED ,
@@ -157,7 +161,6 @@ async def interactive_client(uri: str, **kwargs: Any) -> None:
157161 finally :
158162 incoming .cancel ()
159163 outgoing .cancel ()
160- transport .close ()
161164
162165 await websocket .close ()
163166 assert websocket .close_code is not None and websocket .close_reason is not None
0 commit comments