Bitcoind without docker (#686)

* improving bitcoind code in prep for cypress testing

* implemented datadir and cleanup, improved logging

* reworking cleanup

* ignoring datadir in case of Docker

* black

* fix: better platform-independence killing processes

* added psutil as dependency
This commit is contained in:
Kim Neunert 2020-11-26 20:24:28 +01:00 committed by GitHub
parent 1579d08028
commit 2d4b28402e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 80 deletions

View file

@ -15,3 +15,4 @@ pysocks==1.7.1
six==1.12.0
stem==1.8.0
embit==0.1.1
psutil==5.7.3

View file

@ -209,6 +209,19 @@ protobuf==3.13.0 \
--hash=sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9 \
--hash=sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb \
# via bitbox02
psutil==5.7.3 \
--hash=sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7 \
--hash=sha256:1cd6a0c9fb35ece2ccf2d1dd733c1e165b342604c67454fd56a4c12e0a106787 \
--hash=sha256:2cb55ef9591b03ef0104bedf67cc4edb38a3edf015cf8cf24007b99cb8497542 \
--hash=sha256:56c85120fa173a5d2ad1d15a0c6e0ae62b388bfb956bb036ac231fbdaf9e4c22 \
--hash=sha256:5d9106ff5ec2712e2f659ebbd112967f44e7d33f40ba40530c485cc5904360b8 \
--hash=sha256:6a3e1fd2800ca45083d976b5478a2402dd62afdfb719b30ca46cd28bb25a2eb4 \
--hash=sha256:ade6af32eb80a536eff162d799e31b7ef92ddcda707c27bbd077238065018df4 \
--hash=sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2 \
--hash=sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b \
--hash=sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157 \
--hash=sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7 \
# via -r requirements.in
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f \
# via hwi

View file

@ -7,12 +7,12 @@ if __name__ == "__main__":
# https://flask.palletsprojects.com/en/1.1.x/logging/#basic-configuration
# However the dictConfig doesn't work, so let's do something similiar programatically
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
)
ch.setFormatter(formatter)
logging.getLogger().addHandler(ch)
# However initially, we'll set the root-logger to INFO:
logging.getLogger().setLevel(logging.INFO)
logging.getLogger("cryptoadvance").setLevel(logging.INFO)
cli()

View file

@ -66,7 +66,7 @@ class BitcoindController:
def __init__(self, rpcport=18443):
self.rpcconn = Btcd_conn(rpcport=rpcport)
def start_bitcoind(self, cleanup_at_exit=False):
def start_bitcoind(self, cleanup_at_exit=False, datadir=None):
"""starts bitcoind with a specific rpcport=18543 by default.
That's not the standard in order to make pytest running while
developing locally against a different regtest-instance
@ -76,7 +76,7 @@ class BitcoindController:
return self.check_existing()
logger.debug("Starting bitcoind")
self._start_bitcoind(cleanup_at_exit)
self._start_bitcoind(cleanup_at_exit, datadir=datadir)
self.wait_for_bitcoind(self.rpcconn)
self.mine(block_count=100)
@ -196,8 +196,9 @@ class BitcoindPlainController(BitcoindController):
self.bitcoind_path = bitcoind_path
self.rpcconn.ipaddress = "localhost"
def _start_bitcoind(self, cleanup_at_exit=False):
datadir = tempfile.mkdtemp(prefix="bitcoind_plain_datadir")
def _start_bitcoind(self, cleanup_at_exit=True, datadir=None):
if datadir == None:
datadir = tempfile.mkdtemp(prefix="bitcoind_plain_datadir_")
bitcoind_cmd = self.construct_bitcoind_cmd(
self.rpcconn,
run_docker=False,
@ -212,14 +213,13 @@ class BitcoindPlainController(BitcoindController):
)
def cleanup_bitcoind():
self.bitcoind_proc.kill() # much faster then terminate() and speed is key here over being nice
logger.debug(
self.bitcoind_proc.terminate() # might take a bit longer than kill but it'll preserve block-height
logger.info(
"Killed bitcoind-process with pid {}".format(self.bitcoind_proc.pid)
)
shutil.rmtree(datadir)
logger.debug("removed temp-dir")
if cleanup_at_exit:
logger.debug("REGISTERING EXIT FUNCTIONS")
atexit.register(cleanup_bitcoind)
def stop_bitcoind(self):
@ -251,7 +251,10 @@ class BitcoindDockerController(BitcoindController):
rpcconn, self.btcd_container = self.detect_bitcoind_container(rpcport)
self.rpcconn = rpcconn
def _start_bitcoind(self, cleanup_at_exit):
def _start_bitcoind(self, cleanup_at_exit, datadir=None):
if datadir != None:
# ignored
pass
bitcoind_path = self.construct_bitcoind_cmd(self.rpcconn)
dclient = docker.from_env()
logger.debug("Running (in docker): {}".format(bitcoind_path))
@ -275,6 +278,7 @@ class BitcoindDockerController(BitcoindController):
)
def cleanup_docker_bitcoind():
logger.info("Cleaning up bitcoind-docker-container")
self.btcd_container.stop()
self.btcd_container.remove()

View file

@ -1,7 +1,11 @@
import logging
from logging.config import dictConfig
import os
from os import path
import sys
import psutil
from pathlib import Path
import shutil
import time
from stem.control import Controller
from .util.tor import stop_hidden_services, start_hidden_service
@ -10,7 +14,6 @@ import click
from .server import create_app, init_app
from .helpers import set_loglevel
from os import path
import signal
logger = logging.getLogger(__name__)
@ -187,57 +190,134 @@ def server(daemon, stop, restart, force, port, host, cert, key, debug, tor, hwib
run(debug=debug)
class Echo:
def __init__(self, quiet):
self.quiet = quiet
def echo(self, mystring, prefix=True, **kwargs):
if self.quiet:
pass
else:
if prefix:
click.echo(f" --> ", nl=False)
click.echo(f"{mystring}", **kwargs)
@cli.command()
@click.option("--debug/--no-debug", default=False)
@click.option("--mining/--no-mining", default=True)
@click.option("--docker-tag", "docker_tag", default="latest")
def bitcoind(debug, mining, docker_tag):
@click.option("--debug/--no-debug", default=False, help="Turns on debug-logging")
@click.option("--quiet/--no-quiet", default=False, help="as less output as possible")
@click.option(
"--nodocker", default=False, is_flag=True, help="use without docker (non-default)"
)
@click.option(
"--docker-tag", "docker_tag", default="latest", help="Use a specific docker-tag"
)
@click.option(
"--data-dir",
default="/tmp/bitcoind_plain_datadir",
help="specify a (maybe not yet existing) datadir. Works only in --nodocker (Default:/tmp/bitcoind_plain_datadir) ",
)
@click.option("--mining/--no-mining", default=True, help="Turns on mining (default)")
@click.option(
"--mining-period",
default="15",
help="Every mining-period (in seconds), a block gets mined (default 15sec)",
)
@click.option(
"--reset",
is_flag=True,
default=False,
help="Will kill the bitcoind. Datadir will get lost.",
)
def bitcoind(
debug, quiet, nodocker, docker_tag, data_dir, mining, mining_period, reset
):
"""This will start a bitcoind regtest and mines a block every mining-period.
If a bitcoind is already running on port 18443, it won't start another one. If you CTRL-C this, the bitcoind will
still continue to run. You have to shut it down.
"""
# In order to avoid these dependencies for production use, we're importing them here:
import docker
from .bitcoind import (
BitcoindDockerController,
BitcoindPlainController,
fetch_wallet_addresses_for_mining,
)
from .bitcoind import BitcoindDockerController, fetch_wallet_addresses_for_mining
logging.getLogger().setLevel(logging.INFO)
mining_every_x_seconds = 15
if debug:
logging.getLogger().setLevel(logging.DEBUG)
click.echo(" --> starting or detecting container")
my_bitcoind = BitcoindDockerController(docker_tag=docker_tag)
logging.getLogger("cryptoadvance").setLevel(logging.DEBUG)
logger.debug("Now on debug-logging")
echo = Echo(quiet).echo
if reset:
if not nodocker:
echo("ERROR: --reset only works in conjunction with --nodocker currently")
return
did_something = False
for proc in psutil.process_iter():
try:
# Get process name & pid from process object.
processName = proc.name()
pid = proc.pid
if processName.startswith("bitcoind"):
echo(f"Killing bitcoind-process with id {pid} ...")
did_something = True
os.kill(pid, signal.SIGTERM)
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
echo(f"Pid {pid} not owned by us. Might be a docker-process? {line}")
if Path(data_dir).exists():
echo(f"Purging Datadirectory {data_dir} ...")
did_something = True
shutil.rmtree(data_dir)
if not did_something:
echo("Nothing to do!")
return
logging.getLogger().setLevel(logging.INFO)
mining_every_x_seconds = float(mining_period)
if nodocker:
echo("starting plain bitcoind")
my_bitcoind = BitcoindPlainController()
# Make sure datadir does exist if specified:
Path(data_dir).mkdir(parents=True, exist_ok=True)
else:
echo("starting or detecting container")
my_bitcoind = BitcoindDockerController(docker_tag=docker_tag)
try:
my_bitcoind.start_bitcoind()
my_bitcoind.start_bitcoind(cleanup_at_exit=True, datadir=data_dir)
except docker.errors.ImageNotFound:
click.echo(f" --> Image with tag {docker_tag} does not exist!")
click.echo(
f" --> Try to download first with docker pull \
echo(f"Image with tag {docker_tag} does not exist!")
echo(
f"Try to download first with docker pull \
registry.gitlab.com/cryptoadvance/specter-desktop\
/python-bitcoind:{docker_tag}"
)
sys.exit(1)
tags_of_image = [
image.split(":")[-1] for image in my_bitcoind.btcd_container.image.tags
]
if docker_tag not in tags_of_image:
click.echo(
" --> The running docker container is not \
the tag you requested!"
)
click.echo(
" --> please stop first with docker stop {}".format(
my_bitcoind.btcd_container.id
if not nodocker:
tags_of_image = [
image.split(":")[-1] for image in my_bitcoind.btcd_container.image.tags
]
if docker_tag not in tags_of_image:
echo(
"The running docker container is not \
the tag you requested!"
)
)
sys.exit(1)
click.echo(" --> containerImage: %s" % my_bitcoind.btcd_container.image.tags)
click.echo(" --> url: %s" % my_bitcoind.rpcconn.render_url())
click.echo(" --> user, password: bitcoin, secret")
click.echo(" --> host, port: localhost, 18443")
click.echo(
" --> bitcoin-cli: bitcoin-cli -regtest -rpcuser=bitcoin \
-rpcpassword=secret getblockchaininfo "
echo(
"please stop first with docker stop {}".format(
my_bitcoind.btcd_container.id
)
)
sys.exit(1)
echo("containerImage: %s" % my_bitcoind.btcd_container.image.tags)
echo(" url: %s" % my_bitcoind.rpcconn.render_url())
echo("user, password: bitcoin, secret")
echo(" host, port: localhost, 18443")
echo(
" bitcoin-cli: bitcoin-cli -regtest -rpcuser=bitcoin -rpcpassword=secret getblockchaininfo "
)
if mining:
click.echo(
" --> Now, mining a block every %i seconds. \
Avoid it via --no-mining"
echo(
"Now, mining a block every %f seconds, avoid it via --no-mining"
% mining_every_x_seconds
)
# Get each address some coins
@ -250,40 +330,23 @@ def bitcoind(debug, mining, docker_tag):
# make them spendable
my_bitcoind.mine(block_count=100)
click.echo(" --> ", nl=False)
i = 0
echo(
f"height: {my_bitcoind.rpcconn.get_rpc().getblockchaininfo()['blocks']} | ",
nl=False,
)
i, j = 0, 0
while True:
my_bitcoind.mine()
click.echo("%i" % (i % 10), nl=False)
echo("%i" % (i % 10), prefix=False, nl=False)
if i % 10 == 9:
click.echo(" ", nl=False)
echo(" ", prefix=False, nl=False)
i += 1
if i >= 50:
j = i
i = 0
click.echo(" ")
click.echo(" --> ", nl=False)
echo("", prefix=False)
echo(
f"height: {my_bitcoind.rpcconn.get_rpc().getblockchaininfo()['blocks']} | ",
nl=False,
)
time.sleep(mining_every_x_seconds)
if __name__ == "__main__":
# central and early configuring of logging see
# https://flask.palletsprojects.com/en/1.1.x/logging/#basic-configuration
dictConfig(
{
"version": 1,
"formatters": {
"default": {
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
}
},
"handlers": {
"wsgi": {
"class": "logging.StreamHandler",
"stream": "ext://flask.logging.wsgi_errors_stream",
"formatter": "default",
}
},
"root": {"level": "INFO", "handlers": ["wsgi"]},
}
)
cli()