electrum-personal-server

Maximally lightweight electrum server for a single user
git clone https://git.parazyd.org/electrum-personal-server
Log | Files | Refs | README

commit fd9d32deb9d08da41b4098bee9289e9600380917
parent 196e1452bb3f36469c7b55c4b68d1788027e0ca5
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date:   Fri, 26 Apr 2019 13:37:49 +0100

Make JsonRPC use persistent connections

Now JsonRPC will try to reuse connections instead to creating
a new connection for each RPC call. This solves a problem on
windows where Electrum downloading all the block headers resulted
in crashes because of all the sockets being created and closed.

Diffstat:
Melectrumpersonalserver/server/common.py | 6++++--
Melectrumpersonalserver/server/jsonrpc.py | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
2 files changed, 68 insertions(+), 28 deletions(-)

diff --git a/electrumpersonalserver/server/common.py b/electrumpersonalserver/server/common.py @@ -500,7 +500,8 @@ def run_electrum_server(rpc, txmonitor, config): else: logger.error("IOError: " + repr(e)) try: - sock.close() + if sock != None: + sock.close() except IOError: pass sock = None @@ -712,7 +713,8 @@ def main(): rpc = JsonRpc(host = config.get("bitcoin-rpc", "host"), port = int(config.get("bitcoin-rpc", "port")), user = rpc_u, password = rpc_p, - wallet_filename=config.get("bitcoin-rpc", "wallet_filename").strip()) + wallet_filename=config.get("bitcoin-rpc", "wallet_filename").strip(), + logger=logger) #TODO somewhere here loop until rpc works and fully sync'd, to allow # people to run this script without waiting for their node to fully diff --git a/electrumpersonalserver/server/jsonrpc.py b/electrumpersonalserver/server/jsonrpc.py @@ -1,5 +1,7 @@ -#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket/blob/master/joinmarket/jsonrpc.py -#copyright # Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> and phelix / blockchained.com +# Copyright (C) 2013,2015 by Daniel Kraft <d@domob.eu> +# Copyright (C) 2014 by phelix / blockchained.com + +#jsonrpc.py from https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/jmclient/jmclient/jsonrpc.py import base64 import http.client @@ -9,49 +11,85 @@ class JsonRpcError(Exception): pass class JsonRpcConnectionError(JsonRpcError): pass class JsonRpc(object): - def __init__(self, host, port, user, password, wallet_filename=""): + """ + Simple implementation of a JSON-RPC client that is used + to connect to Bitcoin. + """ + def __init__(self, host, port, user, password, wallet_filename="", + logger=None): self.host = host self.port = port + self.conn = http.client.HTTPConnection(self.host, self.port) self.authstr = "%s:%s" % (user, password) if len(wallet_filename) > 0: self.url = "/wallet/" + wallet_filename else: self.url = "" + self.logger = logger self.queryId = 1 def queryHTTP(self, obj): + """ + Send an appropriate HTTP query to the server. The JSON-RPC + request should be (as object) in 'obj'. If the call succeeds, + the resulting JSON object is returned. In case of an error + with the connection (not JSON-RPC itself), an exception is raised. + """ headers = {"User-Agent": "electrum-personal-server", "Content-Type": "application/json", "Accept": "application/json"} - headers["Authorization"] = "Basic %s" % base64.b64encode( - self.authstr.encode()).decode() + headers["Authorization"] = (b"Basic " + + base64.b64encode(self.authstr.encode('utf-8'))) body = json.dumps(obj) - try: - conn = http.client.HTTPConnection(self.host, self.port) - conn.request("POST", self.url, body, headers) - response = conn.getresponse() - if response.status == 401: - conn.close() - raise JsonRpcConnectionError( - "authentication for JSON-RPC failed") - # All of the codes below are 'fine' from a JSON-RPC point of view. - if response.status not in [200, 404, 500]: - conn.close() - raise JsonRpcConnectionError("unknown error in JSON-RPC") - data = response.read() - conn.close() - return json.loads(data.decode()) - except JsonRpcConnectionError as exc: - raise exc - except Exception as exc: - raise JsonRpcConnectionError("JSON-RPC connection failed. Err:" + - repr(exc)) + for i in range(20): + try: + self.conn.request("POST", self.url, body, headers) + response = self.conn.getresponse() + if response.status == 401: + self.conn.close() + raise JsonRpcConnectionError( + "authentication for JSON-RPC failed") + #All the codes below are 'fine' from a JSON-RPC point of view. + if response.status not in [200, 404, 500]: + self.conn.close() + raise JsonRpcConnectionError("unknown error in JSON-RPC") + data = response.read() + return json.loads(data.decode('utf-8')) + except JsonRpcConnectionError as exc: + raise exc + except http.client.BadStatusLine: + return "CONNFAILURE" + except OSError as e: + self.logger.debug('Reconnecting RPC after error: ' + repr(e)) + self.conn.close() + self.conn.connect() + continue + except Exception as exc: + raise JsonRpcConnectionError("JSON-RPC connection failed. Err:" + + repr(exc)) + break + return None def call(self, method, params): currentId = self.queryId self.queryId += 1 + request = {"method": method, "params": params, "id": currentId} - response = self.queryHTTP(request) + #query can fail from keepalive timeout; keep retrying if it does, up + #to a reasonable limit, then raise (failure to access blockchain + #is a critical failure). Note that a real failure to connect (e.g. + #wrong port) is raised in queryHTTP directly. + response_received = False + for i in range(100): + response = self.queryHTTP(request) + if response != "CONNFAILURE": + response_received = True + break + #Failure means keepalive timed out, just make a new one + self.conn = http.client.HTTPConnection(self.host, self.port) + self.logger.debug("Creating new jsonrpc HTTPConnection") + if not response_received: + raise JsonRpcConnectionError("Unable to connect over RPC") if response["id"] != currentId: raise JsonRpcConnectionError("invalid id returned by query") if response["error"] is not None: