from ephemeral_port_reserve import reserve import logging import re import subprocess import threading import time import os import collections import json import base64 import requests BITCOIND_CONFIG = collections.OrderedDict( [ ("server", 1), ("deprecatedrpc", "addwitnessaddress"), ("addresstype", "p2sh-segwit"), ("deprecatedrpc", "signrawtransaction"), ("rpcuser", "rpcuser"), ("rpcpassword", "rpcpass"), ("listen", 0), ("deprecatedrpc", "generate"), ] ) def write_config(filename, opts): with open(filename, "w") as f: write_dict(f, opts) def write_dict(f, opts): for k, v in opts.items(): if isinstance(v, dict): f.write("[{}]\n".format(k)) write_dict(f, v) else: f.write("{}={}\n".format(k, v)) class TailableProc(object): """A monitorable process that we can start, stop and tail. This is the base class for the daemons. It allows us to directly tail the processes and react to their output. """ def __init__(self, outputDir=None, prefix="proc"): self.logs = [] self.logs_cond = threading.Condition(threading.RLock()) self.cmd_line = None self.running = False self.proc = None self.outputDir = outputDir self.logger = logging.getLogger(prefix) def start(self): """Start the underlying process and start monitoring it. """ self.thread = threading.Thread(target=self.tail) self.thread.daemon = True logging.debug("Starting '%s'", " ".join(self.cmd_line)) self.proc = subprocess.Popen(self.cmd_line, stdout=subprocess.PIPE) self.thread.start() self.running = True def save_log(self): if self.outputDir: logpath = os.path.join(self.outputDir, "log." + str(int(time.time()))) with open(logpath, "w") as f: for l in self.logs: f.write(l + "\n") def stop(self): self.proc.terminate() self.proc.kill() self.save_log() def tail(self): """Tail the stdout of the process and remember it. Stores the lines of output produced by the process in self.logs and signals that a new line was read so that it can be picked up by consumers. """ for line in iter(self.proc.stdout.readline, ""): if len(line) == 0: break with self.logs_cond: self.logs.append(str(line.rstrip())) self.logger.debug(line.decode().rstrip()) self.logs_cond.notifyAll() self.running = False def is_in_log(self, regex): """Look for `regex` in the logs.""" ex = re.compile(regex) for l in self.logs: if ex.search(l): logging.debug("Found '%s' in logs", regex) return True logging.debug("Did not find '%s' in logs", regex) return False def wait_for_log(self, regex, offset=1000, timeout=60): """Look for `regex` in the logs. We tail the stdout of the process and look for `regex`, starting from `offset` lines in the past. We fail if the timeout is exceeded or if the underlying process exits before the `regex` was found. The reason we start `offset` lines in the past is so that we can issue a command and not miss its effects. """ logging.debug("Waiting for '%s' in the logs", regex) ex = re.compile(regex) start_time = time.time() pos = max(len(self.logs) - offset, 0) initial_pos = len(self.logs) while True: if time.time() > start_time + timeout: print("Can't find {} in logs".format(regex)) with self.logs_cond: for i in range(initial_pos, len(self.logs)): print(" " + self.logs[i]) if self.is_in_log(regex): print("(Was previously in logs!") raise TimeoutError('Unable to find "{}" in logs.'.format(regex)) elif not self.running: print("Logs: {}".format(self.logs)) raise ValueError("Process died while waiting for logs") with self.logs_cond: if pos >= len(self.logs): self.logs_cond.wait(1) continue if ex.search(self.logs[pos]): logging.debug("Found '%s' in logs", regex) return self.logs[pos] pos += 1 class BitcoinRpc(object): def __init__(self, url=None, rpcport=8332, rpcuser=None, rpcpassword=None): self.url = url if url else "http://localhost:{}".format(rpcport) authpair = "%s:%s" % (rpcuser, rpcpassword) authpair = authpair.encode("utf8") self.auth_header = b"Basic " + base64.b64encode(authpair) self.__id_count = 0 def _call(self, service_name, *args): self.__id_count += 1 r = requests.post( self.url, data=json.dumps( { "version": "1.1", "method": service_name, "params": args, "id": self.__id_count, } ), headers={ # 'Host': self.__url.hostname, "Authorization": self.auth_header, "Content-type": "application/json", }, ) response = r.json() if response["error"] is not None: raise ValueError(response["error"]) elif "result" not in response: raise ValueError({"code": -343, "message": "missing JSON-RPC result"}) else: return response["result"] def __getattr__(self, name): if name in self.__dict__: return self.__dict__[name] # Create a callable to do the actual call f = lambda *args: self._call(name, *args) # Make debuggers show <function bitcoin.rpc.name> rather than <function # bitcoin.rpc.<lambda>> f.__name__ = name return f class BitcoinD(TailableProc): CONF_NAME = "bitcoin.conf" def __init__(self, bitcoin_dir="/tmp/bitcoind-test", rpcport=None): super().__init__(bitcoin_dir, "bitcoind") if rpcport is None: rpcport = reserve() self.bitcoin_dir = bitcoin_dir self.prefix = "bitcoind" BITCOIND_CONFIG["rpcport"] = rpcport self.rpcport = rpcport self.zmqpubrawblock_port = reserve() self.zmqpubrawtx_port = reserve() regtestdir = os.path.join(bitcoin_dir, "regtest") if not os.path.exists(regtestdir): os.makedirs(regtestdir) conf_file = os.path.join(bitcoin_dir, self.CONF_NAME) self.cmd_line = [ "bitcoind", "-datadir={}".format(bitcoin_dir), "-conf={}".format(conf_file), "-regtest", "-logtimestamps", "-rpcport={}".format(rpcport), "-printtoconsole=1" "-debug", "-rpcuser=rpcuser", "-rpcpassword=rpcpass", "-zmqpubrawblock=tcp://127.0.0.1:{}".format(self.zmqpubrawblock_port), "-zmqpubrawtx=tcp://127.0.0.1:{}".format(self.zmqpubrawtx_port), # "-zmqpubrawblockhwm=0", # "-zmqpubrawtxhwm=0", ] BITCOIND_CONFIG["rpcport"] = rpcport write_config(os.path.join(bitcoin_dir, self.CONF_NAME), BITCOIND_CONFIG) write_config(os.path.join(regtestdir, self.CONF_NAME), BITCOIND_CONFIG) self.rpc = BitcoinRpc(rpcport=rpcport, rpcuser="rpcuser", rpcpassword="rpcpass") def start(self): super().start() self.wait_for_log("Done loading", timeout=10) logging.info("BitcoinD started")