Daemon: reimplement flags and opts using ClassFlags,ClassOpts
This commit is contained in:
parent
f379cde0fc
commit
3a15e62bbe
4 changed files with 135 additions and 59 deletions
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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
107
test/unit_tests_d/ut_daemon.py
Executable 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
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue