7979import threading
8080import sys
8181import os
82- import select
8382import time
84- import socket
8583import socketserver
8684import json
8785import atexit
8886
89- from .ptyproxy import PTYSocketProxy
9087from ..collections import ThreadLocalBox , Shim
9188#from ..misc import async_raise
9289
90+ from .util import mkrecvbuf , recvmsg , sendmsg , ReuseAddrThreadingTCPServer
91+ from .ptyproxy import PTYSocketProxy
92+
9393_server_instance = None
9494_active_connections = set ()
9595_halt_pending = False
@@ -128,13 +128,17 @@ def server_print(*values, **kwargs):
128128 """
129129 print (* values , ** kwargs , file = _original_stdout )
130130
131- class RemoteTabCompletionSession (socketserver .BaseRequestHandler ):
132- """Entry point for connections to the remote tab completion server.
133131
134- In a session, a `RemoteTabCompletionClient` sends us requests. We invoke
135- `rlcompleter` on the server side, and return its response to the client.
132+ class ControlSession (socketserver .BaseRequestHandler ):
133+ """Entry point for connections to the control server.
134+
135+ We use a separate connection for control to avoid head-of-line blocking.
136136
137- For communication, we use JSON encoded dictionaries. This format was chosen
137+ In a session, the client sends us requests for remote tab completion. We
138+ invoke `rlcompleter` on the server side, and return its response to the
139+ client.
140+
141+ We encode the payload as JSON encoded dictionaries. This format was chosen
138142 instead of pickle to ensure the client and server can talk to each other
139143 regardless of the Python versions on each end of the connection.
140144 """
@@ -146,43 +150,24 @@ def handle(self):
146150 class ClientExit (Exception ):
147151 pass
148152 try :
149- server_print ("Remote tab completion session for {} opened." .format (client_address_str ))
153+ server_print ("Control channel for {} opened." .format (client_address_str ))
150154 # TODO: fancier backend? See examples in https://pymotw.com/3/readline/
151155 backend = rlcompleter .Completer (_console_locals_namespace )
156+ buf = mkrecvbuf ()
152157 sock = self .request
153158 while True :
154- rs , ws , es = select .select ([sock ], [], [])
155- for r in rs :
156- # Control channel protocol:
157- # - message-based
158- # - text encoding: utf-8
159- # - message structure: header body, where
160- # header:
161- # 0xFF message start, sync byte (never appears in utf-8 encoded text)
162- # "v": start of protocol version field
163- # one byte containing the version, currently the character "1" as utf-8.
164- # Doesn't need to be a number character, any Unicode codepoint below 127 will do.
165- # It's unlikely more than 127 - 32 = 95 versions of the protocol are ever needed.
166- # "l": start of message length field
167- # netstring containing the length of the message body, as bytes
168- # Netstring format is e.g. "12:hello world!,"
169- # so for example, for a 42-byte message body, this is "2:42,".
170- # The comma is the field terminator.
171- # body:
172- # arbitrary payload, depending on the request.
173- # TODO: must know how to receive until end of message, since TCP doesn't do datagrams
174- # TODO: build a control channel protocol
175- # https://docs.python.org/3/howto/sockets.html
176- data_in = sock .recv (4096 ).decode ("utf-8" )
177- if len (data_in ) == 0 : # EOF on network socket
178- raise ClientExit
179- request = json .loads (data_in )
180- reply = backend .complete (request ["text" ], request ["state" ])
181- # server_print(request, reply)
182- data_out = json .dumps (reply ).encode ("utf-8" )
183- sock .sendall (data_out )
159+ # TODO: Add support for requests to inject Ctrl+C. Needs a command protocol layer.
160+ # TODO: Can use JSON dictionaries; we're guaranteed to get whole messages only.
161+ data_in = recvmsg (buf , sock )
162+ if not data_in :
163+ raise ClientExit
164+ request = json .loads (data_in .decode ("utf-8" ))
165+ reply = backend .complete (request ["text" ], request ["state" ])
166+ # server_print(request, reply)
167+ data_out = json .dumps (reply ).encode ("utf-8" )
168+ sendmsg (data_out , sock )
184169 except ClientExit :
185- server_print ("Remote tab completion session for {} closed." .format (client_address_str ))
170+ server_print ("Control channel for {} closed." .format (client_address_str ))
186171 except BaseException as err :
187172 server_print (err )
188173
@@ -265,17 +250,6 @@ def on_slave_disconnect(adaptor):
265250 _active_connections .remove (id (self ))
266251
267252
268- # https://docs.python.org/3/library/socketserver.html#socketserver.ThreadingTCPServer
269- # https://docs.python.org/3/library/socketserver.html#socketserver.ThreadingMixIn
270- # https://docs.python.org/3/library/socketserver.html#socketserver.TCPServer
271- class ReuseAddrThreadingTCPServer (socketserver .ThreadingTCPServer ):
272- def server_bind (self ):
273- """Custom server_bind ensuring the socket is available for rebind immediately."""
274- # from https://stackoverflow.com/a/18858817
275- self .socket .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
276- self .socket .bind (self .server_address )
277-
278-
279253def start (locals , addrspec = ("127.0.0.1" , 1337 ), banner = None ):
280254 """Start the REPL server.
281255
@@ -349,11 +323,11 @@ def start(locals, addrspec=("127.0.0.1", 1337), banner=None):
349323 server_thread = threading .Thread (target = server .serve_forever , name = "Unpythonic REPL server" , daemon = True )
350324 server_thread .start ()
351325
352- # remote tab completion server
353- # TODO: configurable tab completion port
326+ # control channel for remote tab completion and remote Ctrl+C requests
327+ # TODO: configurable port
354328 # Default is 8128 because it's for *completing* things, and https://en.wikipedia.org/wiki/Perfect_number
355329 # (This is the first one above 1024, and was already known to Nicomachus around 100 CE.)
356- cserver = ReuseAddrThreadingTCPServer ((addr , 8128 ), RemoteTabCompletionSession )
330+ cserver = ReuseAddrThreadingTCPServer ((addr , 8128 ), ControlSession )
357331 cserver .daemon_threads = True
358332 cserver_thread = threading .Thread (target = cserver .serve_forever , name = "Unpythonic REPL remote tab completion server" , daemon = True )
359333 cserver_thread .start ()
0 commit comments