Skip to content

Commit f250911

Browse files
committed
jsonrpcproxy: Unix socket improvements
Lazy connect to unix socket to allow starting the proxy before the backend client. Also handle some common exceptions from unix sockets.
1 parent 9545965 commit f250911

File tree

1 file changed

+89
-20
lines changed

1 file changed

+89
-20
lines changed

scripts/jsonrpcproxy.py

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from http.server import HTTPServer, BaseHTTPRequestHandler
1919
from os import path
2020
from urllib.parse import urlparse
21+
import errno
2122
import socket
2223
import sys
2324

@@ -37,7 +38,62 @@
3738
"""
3839

3940

40-
class NamedPipe(object):
41+
class BackendError(Exception):
42+
pass
43+
44+
45+
class UnixSocketConnector(object):
46+
"""Unix Domain Socket connector. Connects to socket lazily."""
47+
48+
def __init__(self, socket_path):
49+
self.socket_path = socket_path
50+
self.socket = None
51+
52+
@staticmethod
53+
def _get_error_message(os_error_number):
54+
if os_error_number == errno.ENOENT:
55+
return "Unix Domain Socket '{}' does not exist"
56+
if os_error_number == errno.ECONNREFUSED:
57+
return "Connection to '{}' refused"
58+
return "Unknown error when connecting to '{}'"
59+
60+
def _connect(self):
61+
if self.socket is None:
62+
try:
63+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
64+
s.connect(self.socket_path)
65+
s.settimeout(1)
66+
# Assign last, to keep it None in case of exception.
67+
self.socket = s
68+
except OSError as ex:
69+
msg = self._get_error_message(ex.errno)
70+
err = BackendError(msg.format(self.socket_path))
71+
raise err from ex
72+
73+
def _reconnect(self):
74+
self.socket.shutdown(socket.SHUT_RDWR)
75+
self.socket.close()
76+
self.socket = None
77+
self._connect()
78+
79+
def recv(self, max_length):
80+
self._connect()
81+
return self.socket.recv(max_length)
82+
83+
def sendall(self, data):
84+
self._connect()
85+
try:
86+
return self.socket.sendall(data)
87+
except OSError as ex:
88+
if ex.errno == errno.EPIPE:
89+
# The connection was terminated by the backend.
90+
self._reconnect()
91+
return self.socket.sendall(data)
92+
else:
93+
raise
94+
95+
96+
class NamedPipeConnector(object):
4197
"""Windows named pipe simulating socket."""
4298

4399
def __init__(self, ipc_path):
@@ -61,19 +117,20 @@ def close(self):
61117
self.handle.close()
62118

63119

64-
def get_ipc_socket(ipc_path, timeout=1):
120+
def get_ipc_connector(ipc_path):
65121
if sys.platform == 'win32':
66-
return NamedPipe(ipc_path)
67-
68-
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
69-
sock.connect(ipc_path)
70-
sock.settimeout(timeout)
71-
return sock
122+
return NamedPipeConnector(ipc_path)
123+
return UnixSocketConnector(ipc_path)
72124

73125

74126
class HTTPRequestHandler(BaseHTTPRequestHandler):
75127

76128
def do_GET(self):
129+
if self.path != '/':
130+
self.send_response(404)
131+
self.end_headers()
132+
return
133+
77134
self.send_response(200)
78135
self.send_header("Content-type", "text/plain")
79136
self.addCORS()
@@ -96,15 +153,27 @@ def do_POST(self):
96153
# self.log_message("Headers: {}".format(self.headers))
97154
# self.log_message("Request: {}".format(request_content))
98155

99-
response_content = self.server.process(request_content)
100-
# self.log_message("Response: {}".format(response_content))
101-
102-
self.send_response(200)
103-
self.send_header("Content-type", "application/json")
104-
self.send_header("Content-length", len(response_content))
105-
self.addCORS()
106-
self.end_headers()
107-
self.wfile.write(response_content)
156+
try:
157+
response_content = self.server.process(request_content)
158+
# self.log_message("Response: {}".format(response_content))
159+
160+
self.send_response(200)
161+
self.send_header("Content-type", "application/json")
162+
self.send_header("Content-length", len(response_content))
163+
self.addCORS()
164+
self.end_headers()
165+
self.wfile.write(response_content)
166+
except BackendError as err:
167+
self.send_response(502)
168+
error_msg = str(err).encode('utf-8')
169+
# TODO: Send as JSON-RPC response
170+
self.send_header("Content-type", "text/plain")
171+
self.send_header("Content-length", len(error_msg))
172+
self.end_headers()
173+
self.wfile.write(error_msg)
174+
self.log_message("Backend Error: {}".format(err))
175+
176+
# TODO: Handle other excaptions as error 500.
108177

109178
def addCORS(self):
110179
self.send_header("Access-Control-Allow-Origin", "*")
@@ -123,14 +192,14 @@ def __init__(self, proxy_url, backend_path):
123192
super(Proxy, self).__init__(proxy_address, HTTPRequestHandler)
124193

125194
self.backend_address = path.expanduser(backend_path)
126-
self.sock = get_ipc_socket(self.backend_address)
195+
self.conn = get_ipc_connector(self.backend_address)
127196

128197
def process(self, request):
129-
self.sock.sendall(request)
198+
self.conn.sendall(request)
130199

131200
response = b''
132201
while True:
133-
r = self.sock.recv(BUFSIZE)
202+
r = self.conn.recv(BUFSIZE)
134203
if not r:
135204
break
136205
if r[-1] == DELIMITER:

0 commit comments

Comments
 (0)