Daemon: reimplement flags and opts using ClassFlags,ClassOpts

This commit is contained in:
The MMGen Project 2021-08-10 19:38:21 +00:00
commit 3a15e62bbe
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 135 additions and 59 deletions

View file

@ -25,6 +25,7 @@ from subprocess import run,PIPE,CompletedProcess
from collections import namedtuple
from .exception import *
from .common import *
from .flags import *
_dd = namedtuple('daemon_data',['coind_name','coind_version','coind_version_str']) # latest tested version
_cd = namedtuple('coins_data',['coin_name','daemon_ids'])
@ -43,16 +44,18 @@ class Daemon(Lockable):
private_port = None
avail_opts = ()
avail_flags = () # like opts, but can be added or removed after instantiation
_reset_ok = ('debug','wait','_flags')
_reset_ok = ('debug','wait')
def __init__(self,opts=None,flags=None):
def __init__(self):
self.opts = []
self._flags = []
self.platform = g.platform
if self.platform == 'win':
self.use_pidfile = False
self.use_threads = True
self.opt = ClassOpts(self,opts)
self.flag = ClassFlags(self,flags)
def exec_cmd_thread(self,cmd):
import threading
tname = ('exec_cmd','exec_cmd_win_console')[self.platform == 'win' and self.new_console_mswin]
@ -70,7 +73,7 @@ class Daemon(Lockable):
p.wait()
def exec_cmd(self,cmd,is_daemon=False):
out = None if (is_daemon and 'no_daemonize' in self.opts) else PIPE
out = (PIPE,None)[is_daemon and self.opt.no_daemonize]
try:
cp = run(cmd,check=False,stdout=out,stderr=out)
except Exception as e:
@ -87,7 +90,7 @@ class Daemon(Lockable):
if self.debug:
msg('\nExecuting: {}'.format(' '.join(cmd)))
if self.use_threads and (is_daemon and not 'no_daemonize' in self.opts):
if self.use_threads and is_daemon and not self.opt.no_daemonize:
ret = self.exec_cmd_thread(cmd)
else:
ret = self.exec_cmd(cmd,is_daemon)
@ -208,28 +211,10 @@ class Daemon(Lockable):
else:
die(2,f'Wait for state {req_state!r} timeout exceeded for {self.desc} (port {self.bind_port})')
@property
def flags(self):
return self._flags
def add_flag(self,val):
if val not in self.avail_flags:
m = '{!r}: unrecognized flag (available options: {})'
die(1,m.format(val,self.avail_flags))
if val in self._flags:
die(1,'Flag {!r} already set'.format(val))
self._flags.append(val)
def remove_flag(self,val):
if val not in self.avail_flags:
m = '{!r}: unrecognized flag (available options: {})'
die(1,m.format(val,self.avail_flags))
if val not in self._flags:
die(1,'Flag {!r} not set, so cannot be removed'.format(val))
self._flags.remove(val)
class RPCDaemon(Daemon):
avail_opts = ('no_daemonize',)
def __init__(self):
super().__init__()
self.desc = '{} {} {}RPC daemon'.format(
@ -305,7 +290,7 @@ class MoneroWalletDaemon(RPCDaemon):
[f'--daemon-port={self.daemon_port}', not self.daemon_addr],
[f'--proxy={self.proxy}', self.proxy],
[f'--pidfile={self.pidfile}', self.platform == 'linux'],
['--detach', not 'no_daemonize' in self.opts],
['--detach', not self.opt.no_daemonize],
['--stagenet', self.network == 'testnet'],
)
@ -337,10 +322,10 @@ class CoinDaemon(Daemon):
def __new__(cls,
network_id = None,
test_suite = False,
flags = None,
proto = None,
opts = None,
flags = None,
test_suite = False,
port_shift = None,
p2p_port = None,
datadir = None,
@ -378,10 +363,10 @@ class CoinDaemon(Daemon):
def __init__(self,
network_id = None,
test_suite = False,
flags = None,
proto = None,
opts = None,
flags = None,
test_suite = False,
port_shift = None,
p2p_port = None,
datadir = None,
@ -389,7 +374,7 @@ class CoinDaemon(Daemon):
self.test_suite = test_suite
super().__init__()
super().__init__(opts=opts,flags=flags)
self._set_ok += ('shared_args','usr_coind_args')
self.shared_args = []
@ -403,22 +388,6 @@ class CoinDaemon(Daemon):
getattr(self.proto.network_names,self.network),
'test suite ' if test_suite else '' )
if opts:
if type(opts) not in (list,tuple):
die(1,f'{opts!r}: illegal value for opts (must be list or tuple)')
for o in opts:
if o not in CoinDaemon.avail_opts:
die(1,f'{o!r}: unrecognized option')
elif o not in self.avail_opts:
die(1,f'{o!r}: option not supported for {self.coind_name} daemon')
self.opts = list(opts)
if flags:
if type(flags) not in (list,tuple):
die(1,f'{flags!r}: illegal value for flags (must be list or tuple)')
for flag in flags:
self.add_flag(flag)
# user-set values take precedence
self.datadir = os.path.abspath(datadir or g.daemon_data_dir or self.init_datadir())
self.non_dfl_datadir = bool(datadir or g.daemon_data_dir or test_suite)
@ -475,7 +444,7 @@ class CoinDaemon(Daemon):
def pre_start(self):
os.makedirs(self.datadir,exist_ok=True)
if self.cfg_file and not 'keep_cfg_file' in self.flags:
if self.cfg_file and not self.flag.keep_cfg_file:
open('{}/{}'.format(self.datadir,self.cfg_file),'w').write(self.cfg_file_hdr)
if self.use_pidfile and os.path.exists(self.pidfile):
@ -536,7 +505,7 @@ class bitcoin_core_daemon(CoinDaemon):
['--rpcallowip=127.0.0.1'],
[f'--rpcbind=127.0.0.1:{self.rpc_port}'],
['--pid='+self.pidfile, self.use_pidfile],
['--daemon', self.platform == 'linux' and not 'no_daemonize' in self.opts],
['--daemon', self.platform == 'linux' and not self.opt.no_daemonize],
['--fallbackfee=0.0002', self.coin == 'BTC' and self.network == 'regtest'],
['--usecashaddr=0', self.coin == 'BCH'],
['--mempoolreplacement=1', self.coin == 'LTC'],
@ -628,8 +597,8 @@ class monero_daemon(CoinDaemon):
['--no-igd'],
[f'--data-dir={self.datadir}', self.non_dfl_datadir],
[f'--pidfile={self.pidfile}', self.platform == 'linux'],
['--detach', not 'no_daemonize' in self.opts],
['--offline', not 'online' in self.opts],
['--detach', not self.opt.no_daemonize],
['--offline', not self.opt.online],
)
@property
@ -645,7 +614,7 @@ class ethereum_daemon(CoinDaemon):
def __init__(self,*args,**kwargs):
if not hasattr(ethereum_daemon,'all_daemons'):
if not hasattr(self,'all_daemons'):
ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon,names=True)
self.port_offset = (
@ -679,7 +648,7 @@ class openethereum_daemon(ethereum_daemon):
def init_subclass(self):
ld = self.platform == 'linux' and not 'no_daemonize' in self.opts
ld = self.platform == 'linux' and not self.opt.no_daemonize
self.coind_args = list_gen(
['--no-ws'],
@ -719,7 +688,7 @@ class geth_daemon(ethereum_daemon):
['--http.api=eth,web3,txpool'],
[f'--http.port={self.rpc_port}'],
[f'--port={self.p2p_port}', self.p2p_port], # geth binds p2p port even with --maxpeers=0
['--maxpeers=0', not 'online' in self.opts],
['--maxpeers=0', not self.opt.online],
[f'--datadir={self.datadir}', self.non_dfl_datadir],
['--goerli', self.network=='testnet'],
['--dev', self.network=='regtest'],
@ -727,7 +696,6 @@ class geth_daemon(ethereum_daemon):
# https://github.com/ledgerwatch/erigon
class erigon_daemon(geth_daemon):
avail_opts = ('online',)
daemon_data = _dd('Erigon', 2021007005, '2021.07.5')
version_pat = r'erigon/(\d+)\.(\d+)\.(\d+)'
exec_fn = 'erigon'
@ -741,7 +709,7 @@ class erigon_daemon(geth_daemon):
self.coind_args = list_gen(
['--verbosity=0'],
[f'--port={self.p2p_port}', self.p2p_port],
['--maxpeers=0', not 'online' in self.opts],
['--maxpeers=0', not self.opt.online],
[f'--private.api.addr=127.0.0.1:{self.private_port}'],
[f'--datadir={self.datadir}', self.non_dfl_datadir],
['--chain=dev', self.network=='regtest'],

View file

@ -21,6 +21,7 @@ opts_data = {
-s, --get-state Get the state of the daemon(s) and exit
-t, --testing Testing mode. Print commands but don't execute them
-q, --quiet Produce quieter output
-u, --usermode Run the daemon in user (non test-suite) mode
-v, --verbose Produce more verbose output
-W, --no-wait Don't wait for daemons to change state before exiting
""",
@ -42,7 +43,7 @@ def run(network_id=None,proto=None,daemon_id=None):
d = CoinDaemon(
network_id = network_id,
proto = proto,
test_suite = True,
test_suite = not opt.usermode,
opts = ['no_daemonize'] if opt.no_daemonize else None,
port_shift = int(opt.port_shift or 0),
datadir = opt.datadir,

107
test/unit_tests_d/ut_daemon.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
test/unit_tests_d/ut_daemon.py: unit test for the MMGen suite's Daemon class
"""
from subprocess import run,DEVNULL
from mmgen.common import *
from mmgen.exception import *
from mmgen.daemon import *
from mmgen.protocol import init_proto
class unit_test(object):
def run_test(self,name,ut):
def test_flags():
d = CoinDaemon('eth')
vmsg(f'Available opts: {fmt_list(d.avail_opts,fmt="bare")}')
vmsg(f'Available flags: {fmt_list(d.avail_flags,fmt="bare")}')
vals = namedtuple('vals',['online','no_daemonize','keep_cfg_file'])
def gen():
for opts,flags,val in (
(None,None, vals(False,False,False)),
(None,['keep_cfg_file'], vals(False,False,True)),
(['online'],['keep_cfg_file'], vals(True,False,True)),
(['online','no_daemonize'],['keep_cfg_file'], vals(True,True,True)),
):
d = CoinDaemon('eth',opts=opts,flags=flags)
assert d.flag.keep_cfg_file == val.keep_cfg_file
assert d.opt.online == val.online
assert d.opt.no_daemonize == val.no_daemonize
d.flag.keep_cfg_file = not val.keep_cfg_file
d.flag.keep_cfg_file = val.keep_cfg_file
yield d
return tuple(gen())
def test_flags_err(d):
def bad1(): d[0].flag.foo = False
def bad2(): d[0].opt.foo = False
def bad3(): d[0].opt.no_daemonize = True
def bad4(): d[0].flag.keep_cfg_file = 'x'
def bad5(): d[0].opt.no_daemonize = 'x'
def bad6(): d[0].flag.keep_cfg_file = False
def bad7(): d[1].flag.keep_cfg_file = True
ut.process_bad_data((
('flag (1)', 'ClassFlagsError', 'unrecognized flag', bad1 ),
('opt (1)', 'ClassFlagsError', 'unrecognized opt', bad2 ),
('opt (2)', 'AttributeError', 'is read-only', bad3 ),
('flag (2)', 'AssertionError', 'not boolean', bad4 ),
('opt (3)', 'AttributeError', 'is read-only', bad5 ),
('flag (3)', 'ClassFlagsError', 'not set', bad6 ),
('flag (4)', 'ClassFlagsError', 'already set', bad7 ),
))
def test_cmds(op):
network_ids = CoinDaemon.get_network_ids()
for test_suite in [True,False] if op == 'print' else [True]:
vmsg(orange(f'Start commands (op={op}, test_suite={test_suite}):'))
for coin,data in CoinDaemon.coins.items():
for daemon_id in data.daemon_ids:
for network in globals()[daemon_id+'_daemon'].networks:
d = CoinDaemon(
proto=init_proto(coin=coin,network=network),
daemon_id = daemon_id,
test_suite = test_suite )
if op == 'print':
for cmd in d.start_cmds:
vmsg(' '.join(cmd))
else:
if run(['which',d.exec_fn],stdout=DEVNULL,stderr=DEVNULL).returncode:
if op == 'start':
qmsg(yellow(f'Warning: {d.exec_fn} not found in executable path'))
else:
if opt.quiet:
msg_r('.')
getattr(d,op)(silent=opt.quiet)
qmsg_r('Testing flags and opts...')
vmsg('')
daemons = test_flags()
qmsg('OK')
qmsg_r('Testing error handling for flags and opts...')
vmsg('')
test_flags_err(daemons)
qmsg('OK')
qmsg_r('Testing start commands for configured daemons...')
vmsg('')
test_cmds('print')
qmsg('OK')
msg_r('Starting all configured daemons available on system...')
qmsg('')
test_cmds('start')
msg('OK')
msg_r('Stopping all configured daemons available on system...')
qmsg('')
test_cmds('stop')
msg('OK')
return True

View file

@ -18,7 +18,7 @@ def cfg_file_auth_test(proto,d):
cf = os.path.join(d.datadir,d.cfg_file)
open(cf,'a').write('\nrpcuser = ut_rpc\nrpcpassword = ut_rpc_passw0rd\n')
d.add_flag('keep_cfg_file')
d.flag.keep_cfg_file = True
d.start()
async def do():