new TestDaemon API for controlling test-suite & regtest daemons

This commit is contained in:
The MMGen Project 2019-12-07 12:33:01 +00:00
commit f18b14d395
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
3 changed files with 296 additions and 0 deletions

View file

@ -49,6 +49,7 @@ class WalletFileError(Exception): mmcode = 3
class HexadecimalStringError(Exception): mmcode = 3
class SeedLengthError(Exception): mmcode = 3
class PrivateKeyError(Exception): mmcode = 3
class MMGenCalledProcessError(Exception): mmcode = 3
# 4: red hl, 'MMGen Fatal Error' + exception + message
class BadMMGenTxID(Exception): mmcode = 4

294
mmgen/test_daemon.py Executable file
View file

@ -0,0 +1,294 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
test_daemon.py: Daemon control classes for MMGen test suite and regtest mode
"""
from subprocess import run,PIPE
from mmgen.exception import *
from mmgen.common import *
class TestDaemon(MMGenObject):
cfg_file_hdr = ''
subclasses_must_implement = ('state','do_stop','stop_cmd')
network_ids = ('btc','btc_tn','bch','bch_tn','ltc','ltc_tn','xmr')
debug = False
wait = True
use_pidfile = True
testnet_arg = []
coind_args = []
cli_args = []
shared_args = []
coin_specific_coind_args = []
coin_specific_cli_args = []
coin_specific_shared_args = []
usr_coind_args = []
usr_cli_args = []
usr_shared_args = []
usr_rpc_port = None
def __new__(cls,network_id,datadir,desc='test suite daemon',rpc_port=None):
assert network_id in cls.network_ids, '{!r}: invalid network ID'.format(network_id)
assert os.path.isabs(datadir), '{!r}: invalid datadir (not an absolute path)'.format(datadir)
if network_id.endswith('_tn'):
coinsym = network_id[:-3]
network = 'testnet'
else:
coinsym = network_id
network = 'mainnet'
me = MMGenObject.__new__((BitcoinTestDaemon,MoneroTestDaemon)[coinsym=='xmr'])
me.network_id = network_id
me.coinsym = coinsym
me.network = network
me.datadir = datadir
me.platform = g.platform
me.desc = desc
me.usr_rpc_port = rpc_port
return me
def __init__(self,network_id,datadir,desc='test suite daemon',rpc_port=None):
self.pidfile = '{}/{}-daemon.pid'.format(self.datadir,self.network)
self.coin,self.coind_exec,self.cli_exec,self.conf_file = {
'btc': ('Bitcoin', 'bitcoind', 'bitcoin-cli', 'bitcoin.conf'),
'ltc': ('Litecoin','litecoind', 'litecoin-cli','litecoin.conf'),
'bch': ('Bcash', 'bitcoind-abc','bitcoin-cli', 'bitcoin.conf'),
'xmr': ('Monero', 'monerod', 'monerod', 'bitmonero.conf')
}[self.coinsym]
self.net_desc = '{} {}'.format(self.coin,self.network)
def exec_cmd_thread(self,cmd,check):
import threading
t = threading.Thread(target=self.exec_cmd,args=(cmd,check))
t.daemon = True
t.start()
Msg_r(' \b') # blocks w/o this...crazy
def exec_cmd(self,cmd,check):
cp = run(cmd,check=False,stdout=PIPE,stderr=PIPE)
if check and cp.returncode != 0:
raise MMGenCalledProcessError(cp)
return cp
def run_cmd(self,cmd,silent=False,check=True,is_daemon=False):
if is_daemon and not silent:
msg('Starting {} {}'.format(self.net_desc,self.desc))
if self.debug:
msg('\nExecuting: {}'.format(' '.join(cmd)))
if self.platform == 'win' and is_daemon:
cp = self.exec_cmd_thread(cmd,check)
else:
cp = self.exec_cmd(cmd,check)
if cp:
out = cp.stdout.decode().rstrip()
err = cp.stderr.decode().rstrip()
if out and (self.debug or not silent):
msg(out)
if err and (self.debug or (cp.returncode and not silent)):
msg(err)
return cp
@property
def pid(self):
return open(self.pidfile).read().strip() if self.use_pidfile else '(unknown)'
def cmd(self,action,*args,**kwargs):
return getattr(self,action)(*args,**kwargs)
@property
def start_cmd(self):
return ([self.coind_exec]
+ self.testnet_arg
+ self.coind_args
+ self.shared_args
+ self.coin_specific_coind_args
+ self.coin_specific_shared_args
+ self.usr_coind_args
+ self.usr_shared_args)
def cli_cmd(self,*cmds):
return ([self.cli_exec]
+ self.testnet_arg
+ self.cli_args
+ self.shared_args
+ self.coin_specific_cli_args
+ self.coin_specific_shared_args
+ self.usr_cli_args
+ self.usr_shared_args
+ list(cmds))
def do_start(self,silent=False):
return self.run_cmd(self.start_cmd,silent=silent,is_daemon=True)
def cli(self,*cmds,silent=False,check=True):
return self.run_cmd(self.cli_cmd(*cmds),silent=silent,check=check)
def start(self,silent=False):
if self.is_ready:
if not silent:
m = '{} {} already running with pid {}'
msg(m.format(self.net_desc,self.desc,self.pid))
else:
os.makedirs(self.datadir,exist_ok=True)
if self.conf_file:
open('{}/{}'.format(self.datadir,self.conf_file),'w').write(self.cfg_file_hdr)
ret = self.do_start(silent=silent)
if self.wait:
self.wait_for_state('ready')
return ret
def stop(self,silent=False):
if self.is_ready:
ret = self.do_stop(silent=silent)
if self.wait:
self.wait_for_state('stopped')
return ret
else:
if not silent:
msg('{} {} not running'.format(self.net_desc,self.desc))
# rm -rf $datadir
def wait_for_state(self,req_state):
for i in range(200):
if self.state == req_state:
return True
time.sleep(0.2)
else:
die(2,'Daemon wait timeout for {} {} exceeded'.format(self.coin,self.network))
@property
def is_ready(self):
return self.state == 'ready'
@classmethod
def check_implement(cls):
m = 'required method {}() missing in class {}'
for subcls in cls.__subclasses__():
for k in cls.subclasses_must_implement:
assert k in subcls.__dict__, m.format(k,subcls.__name__)
class BitcoinTestDaemon(TestDaemon):
dtype = 'bitcoin'
cfg_file_hdr = '# TestDaemon config file\n'
def __init__(self,network_id,datadir,desc='test suite daemon',rpc_port=None):
super().__init__(network_id,datadir,desc=desc)
if self.platform == 'win' and self.coinsym == 'bch':
self.use_pidfile = False
if self.network=='testnet':
self.testnet_arg = ['--testnet']
self.shared_args = ['--datadir='+self.datadir]
if self.usr_rpc_port:
self.shared_args += ['--rpcport={}'.format(self.usr_rpc_port)]
self.coind_args = ['--listen=0','--keypool=1']
if self.use_pidfile:
self.coind_args += ['--pid='+self.pidfile]
if self.platform == 'linux':
self.coind_args += ['--daemon']
if self.coinsym == 'bch':
port = self.usr_rpc_port or (8442,18442)[self.network=='testnet']
self.coin_specific_coind_args = [
'--rpcallowip=127.0.0.1',
'--rpcbind=127.0.0.1:{}'.format(port),
'--usecashaddr=0' ]
if not self.usr_rpc_port:
self.coin_specific_cli_args = ['--rpcport={}'.format(port)]
elif self.coinsym == 'ltc':
self.coin_specific_coind_args = ['--mempoolreplacement=1']
@property
def state(self):
cp = self.cli('getblockcount',silent=True,check=False)
err = cp.stderr.decode()
if ("error: couldn't connect" in err
or "error: Could not connect" in err
or "does not exist" in err ):
return 'stopped'
elif cp.returncode == 0:
return 'ready'
else:
return 'busy'
@property
def stop_cmd(self):
return self.cli_cmd('stop')
def do_stop(self,silent=False):
if not silent:
msg('Stopping {} {}'.format(self.net_desc,self.desc))
return self.cli('stop',silent=True)
class MoneroTestDaemon(TestDaemon):
dtype = 'monero'
def __init__(self,network_id,datadir,desc='test suite daemon',rpc_port=None):
super().__init__(network_id,datadir,desc=desc)
self.shared_args = ['--zmq-rpc-bind-port=18182','--rpc-bind-port=18181']
self.coind_args = [ '--bg-mining-enable',
'--pidfile='+self.pidfile,
'--data-dir='+self.datadir,
'--detach',
'--offline' ]
@property
def state(self):
cp = self.run_cmd(
[self.coind_exec]
+ self.shared_args
+ ['status'],
silent=True,
check=False )
return 'stopped' if 'Error:' in cp.stdout.decode() else 'ready'
def do_start(self,silent=False):
return super().do_start(silent=silent)
@property
def stop_cmd(self):
return [self.coind_exec] + self.shared_args + ['exit']
def do_stop(self,silent=False):
return self.run_cmd(self.stop_cmd,silent=silent)
TestDaemon.check_implement()

View file

@ -113,6 +113,7 @@ setup(
'mmgen.seed',
'mmgen.sha2',
'mmgen.term',
'mmgen.test_daemon',
'mmgen.tool',
'mmgen.tw',
'mmgen.tx',