@@ -23,46 +23,73 @@ def _handle_alarm(signum, frame):
2323signal .signal (signal .SIGALRM , _handle_alarm )
2424
2525
26- def _make_remote_completion_client (sock ):
27- """Make a tab completion function for a remote REPL session.
28-
29- `buf` is a receive buffer for the message protocol (see
30- `unpythonic.net.msg.ReceiveBuffer`).
31-
32- `sock` must be a socket already connected to a `ControlSession`.
33- The caller is responsible for managing the socket.
34-
35- The return value can be used as a completer in `readline.set_completer`.
36- """
37- # Messages must be processed by just one central decoder, to prevent data
38- # races, but also to handle buffering of incoming data correctly, because
39- # the underlying transport (TCP) has no notion of message boundaries. Thus
40- # typically, at the seam between two messages, some of the data belonging
41- # to the beginning of the second message has already arrived and been read
42- # from the socket, when trying to read the last batch of data belonging to
43- # the end of the first message.
44- #
45- # The source is just an abstraction over the details of how to actually
46- # read data from a specific type of data source; buffering occurs in
47- # ReceiveBuffer inside MessageDecoder.
48- decoder = MessageDecoder (socketsource (sock ))
49- def complete (text , state ):
26+ # Messages must be processed by just one central decoder, to prevent data
27+ # races, but also to handle buffering of incoming data correctly, because
28+ # the underlying transport (TCP) has no notion of message boundaries. Thus
29+ # typically, at the seam between two messages, some of the data belonging
30+ # to the beginning of the second message has already arrived and been read
31+ # from the socket, when trying to read the last batch of data belonging to
32+ # the end of the first message.
33+ class ControlClient :
34+ # TODO: manage the socket internally. We need to make this into a context manager,
35+ # so that __enter__ can set up the socket and __exit__ can tear it down.
36+ def __init__ (self , sock ):
37+ """Initialize session for control channel (client side).
38+
39+ `sock` must be a socket already connected to a `ControlSession` (on the
40+ server side). The caller is responsible for managing the socket.
41+ """
42+ self .sock = sock
43+ # The source is just an abstraction over the details of how to actually
44+ # read data from a specific type of data source; buffering occurs in
45+ # ReceiveBuffer inside MessageDecoder.
46+ self .decoder = MessageDecoder (socketsource (sock ))
47+
48+ # TODO: Refactor low-level _send, _recv functions.
49+ # TODO: Always JSON encode. Don't send just strings.
50+
51+ def complete (self , text , state ):
52+ """Tab-complete in a remote REPL session.
53+
54+ This is a completer for `readline.set_completer`.
55+ """
5056 try :
51- request = {"text" : text , "state" : state }
57+ request = {"command" : "TabComplete" , " text" : text , "state" : state }
5258 data_out = json .dumps (request ).encode ("utf-8" )
53- sock .sendall (encodemsg (data_out ))
54- data_in = decoder .decode ()
59+ self . sock .sendall (encodemsg (data_out ))
60+ data_in = self . decoder .decode ()
5561 response = data_in .decode ("utf-8" )
5662 # print("text '{}' state '{}' reply '{}'".format(text, state, response))
5763 if not response :
58- print ("Control server exited, socket closed! " )
64+ print ("Socket closed by other end. " )
5965 return None
6066 reply = json .loads (response )
6167 return reply
6268 except BaseException as err :
6369 print (type (err ), err )
6470 return None
65- return complete
71+
72+ def send_kbinterrupt (self ):
73+ """Request the server to perform a `KeyboardInterrupt` (Ctrl+C).
74+
75+ The `KeyboardInterrupt` occurs in the REPL session associated with
76+ this control channel.
77+
78+ Returns `True` on success, `False` on failure.
79+ """
80+ try :
81+ request = {"command" : "KeyboardInterrupt" }
82+ data_out = json .dumps (request ).encode ("utf-8" )
83+ self .sock .sendall (encodemsg (data_out ))
84+ data_in = self .decoder .decode ()
85+ response = data_in .decode ("utf-8" )
86+ if not response :
87+ print ("Socket closed by other end." )
88+ return False
89+ return response == "ok"
90+ except BaseException as err :
91+ print (type (err ), err )
92+ return False
6693
6794
6895def connect (addrspec ):
@@ -91,8 +118,8 @@ class SessionExit(Exception):
91118
92119 # Set a custom tab completer for readline.
93120 # https://stackoverflow.com/questions/35115208/is-there-any-way-to-combine-readline-rlcompleter-and-interactiveconsole-in-pytho
94- completer = _make_remote_completion_client (csock )
95- readline .set_completer (completer )
121+ controller = ControlClient (csock )
122+ readline .set_completer (controller . complete )
96123 readline .parse_and_bind ("tab: complete" )
97124
98125 def sock_to_stdout ():
@@ -134,8 +161,7 @@ def sock_to_stdout():
134161 inp = input ()
135162 sock .sendall ((inp + "\n " ).encode ("utf-8" ))
136163 except KeyboardInterrupt :
137- # TODO: refactor control channel logic; send command on control channel
138- sock .sendall ("\x03 \n " .encode ("utf-8" ))
164+ controller .send_kbinterrupt ()
139165 except EOFError :
140166 print ("replclient: Ctrl+D pressed, asking server to disconnect." )
141167 print ("replclient: if the server does not respond, press Ctrl+C to force." )
0 commit comments