Browse Source

new TestDaemon API for controlling test-suite & regtest daemons

The MMGen Project 5 years ago
parent
commit
f18b14d395
3 changed files with 296 additions and 0 deletions
  1. 1 0
      mmgen/exception.py
  2. 294 0
      mmgen/test_daemon.py
  3. 1 0
      setup.py

+ 1 - 0
mmgen/exception.py

@@ -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 - 0
mmgen/test_daemon.py

@@ -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()

+ 1 - 0
setup.py

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