daemon.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. daemon.py: Daemon control interface for the MMGen suite
  20. """
  21. import shutil
  22. from subprocess import run,PIPE
  23. from collections import namedtuple
  24. from mmgen.exception import *
  25. from mmgen.common import *
  26. class Daemon(MMGenObject):
  27. cfg_file_hdr = ''
  28. subclasses_must_implement = ('state','stop_cmd')
  29. network_ids = ('btc','btc_tn','bch','bch_tn','ltc','ltc_tn','xmr','eth','etc')
  30. cd = namedtuple('coin_data',['coin','coind_exec','cli_exec','conf_file','dfl_rpc','dfl_rpc_tn'])
  31. coins = {
  32. 'btc': cd('Bitcoin', 'bitcoind', 'bitcoin-cli', 'bitcoin.conf', 8333,18333),
  33. 'bch': cd('Bcash', 'bitcoind-abc','bitcoin-cli', 'bitcoin.conf', 8442,18442), # MMGen RPC dfls
  34. 'ltc': cd('Litecoin', 'litecoind', 'litecoin-cli','litecoin.conf', 9333,19335),
  35. 'xmr': cd('Monero', 'monerod', 'monerod', 'bitmonero.conf',18082,28082),
  36. 'eth': cd('Ethereum', 'parity', 'parity', 'parity.conf', 8545,8545),
  37. 'etc': cd('Ethereum Classic','parity', 'parity', 'parity.conf', 8545,8545)
  38. }
  39. port_shift = 1000
  40. debug = False
  41. wait = True
  42. use_pidfile = True
  43. testnet_arg = []
  44. coind_args = []
  45. cli_args = []
  46. shared_args = []
  47. coind_cmd = []
  48. coin_specific_coind_args = []
  49. coin_specific_cli_args = []
  50. coin_specific_shared_args = []
  51. usr_coind_args = []
  52. usr_cli_args = []
  53. usr_shared_args = []
  54. usr_rpc_port = None
  55. def __new__(cls,network_id,datadir=None,rpc_port=None,desc='test suite daemon'):
  56. network_id = network_id.lower()
  57. assert network_id in cls.network_ids, '{!r}: invalid network ID'.format(network_id)
  58. if not datadir: # hack for throwaway instances
  59. datadir = '/tmp/foo'
  60. assert os.path.isabs(datadir), '{!r}: invalid datadir (not an absolute path)'.format(datadir)
  61. if network_id.endswith('_tn'):
  62. coinsym = network_id[:-3]
  63. network = 'testnet'
  64. else:
  65. coinsym = network_id
  66. network = 'mainnet'
  67. me = MMGenObject.__new__(
  68. MoneroDaemon if coinsym == 'xmr'
  69. else EthereumDaemon if coinsym in ('eth','etc')
  70. else BitcoinDaemon )
  71. me.network_id = network_id
  72. me.coinsym = coinsym
  73. me.network = network
  74. me.datadir = datadir
  75. me.platform = g.platform
  76. me.desc = desc
  77. me.usr_rpc_port = rpc_port
  78. return me
  79. def __init__(self,network_id,datadir=None,rpc_port=None,desc='test suite daemon'):
  80. self.pidfile = '{}/{}-daemon.pid'.format(self.datadir,self.network)
  81. for k in self.coins[self.coinsym]._fields:
  82. setattr(self,k,getattr(self.coins[self.coinsym],k))
  83. self.rpc_port = self.usr_rpc_port or (
  84. (self.dfl_rpc,self.dfl_rpc_tn)[self.network=='testnet'] + self.port_shift
  85. )
  86. self.net_desc = '{} {}'.format(self.coin,self.network)
  87. self.subclass_init()
  88. def subclass_init(self): pass
  89. def exec_cmd_thread(self,cmd,check):
  90. import threading
  91. t = threading.Thread(target=self.exec_cmd,args=(cmd,check))
  92. t.daemon = True
  93. t.start()
  94. Msg_r(' \b') # blocks w/o this...crazy
  95. def exec_cmd(self,cmd,check):
  96. cp = run(cmd,check=False,stdout=PIPE,stderr=PIPE)
  97. if check and cp.returncode != 0:
  98. raise MMGenCalledProcessError(cp)
  99. return cp
  100. def run_cmd(self,cmd,silent=False,check=True,is_daemon=False):
  101. if is_daemon and not silent:
  102. msg('Starting {} {}'.format(self.net_desc,self.desc))
  103. if self.debug:
  104. msg('\nExecuting: {}'.format(' '.join(cmd)))
  105. if self.platform == 'win' and is_daemon:
  106. cp = self.exec_cmd_thread(cmd,check)
  107. else:
  108. cp = self.exec_cmd(cmd,check)
  109. if cp:
  110. out = cp.stdout.decode().rstrip()
  111. err = cp.stderr.decode().rstrip()
  112. if out and (self.debug or not silent):
  113. msg(out)
  114. if err and (self.debug or (cp.returncode and not silent)):
  115. msg(err)
  116. return cp
  117. @property
  118. def pid(self):
  119. return open(self.pidfile).read().strip() if self.use_pidfile else '(unknown)'
  120. def cmd(self,action,*args,**kwargs):
  121. return getattr(self,action)(*args,**kwargs)
  122. @property
  123. def start_cmd(self):
  124. return ([self.coind_exec]
  125. + self.testnet_arg
  126. + self.coind_args
  127. + self.shared_args
  128. + self.coin_specific_coind_args
  129. + self.coin_specific_shared_args
  130. + self.usr_coind_args
  131. + self.usr_shared_args
  132. + self.coind_cmd )
  133. def cli_cmd(self,*cmds):
  134. return ([self.cli_exec]
  135. + self.testnet_arg
  136. + self.cli_args
  137. + self.shared_args
  138. + self.coin_specific_cli_args
  139. + self.coin_specific_shared_args
  140. + self.usr_cli_args
  141. + self.usr_shared_args
  142. + list(cmds))
  143. def do_start(self,silent=False):
  144. if not silent:
  145. msg('Starting {} {}'.format(self.net_desc,self.desc))
  146. return self.run_cmd(self.start_cmd,silent=True,is_daemon=True)
  147. def do_stop(self,silent=False):
  148. if not silent:
  149. msg('Stopping {} {}'.format(self.net_desc,self.desc))
  150. return self.run_cmd(self.stop_cmd,silent=True)
  151. def cli(self,*cmds,silent=False,check=True):
  152. return self.run_cmd(self.cli_cmd(*cmds),silent=silent,check=check)
  153. def start(self,silent=False):
  154. if self.is_ready:
  155. if not silent:
  156. m = '{} {} already running with pid {}'
  157. msg(m.format(self.net_desc,self.desc,self.pid))
  158. else:
  159. os.makedirs(self.datadir,exist_ok=True)
  160. if self.conf_file:
  161. open('{}/{}'.format(self.datadir,self.conf_file),'w').write(self.cfg_file_hdr)
  162. if self.use_pidfile and os.path.exists(self.pidfile):
  163. # Parity just overwrites the data in an existing pidfile, leading to
  164. # interesting consequences.
  165. os.unlink(self.pidfile)
  166. ret = self.do_start(silent=silent)
  167. if self.wait:
  168. self.wait_for_state('ready')
  169. return ret
  170. def stop(self,silent=False):
  171. if self.is_ready:
  172. ret = self.do_stop(silent=silent)
  173. if self.wait:
  174. self.wait_for_state('stopped')
  175. return ret
  176. else:
  177. if not silent:
  178. msg('{} {} not running'.format(self.net_desc,self.desc))
  179. # rm -rf $datadir
  180. def wait_for_state(self,req_state):
  181. for i in range(200):
  182. if self.state == req_state:
  183. return True
  184. time.sleep(0.2)
  185. else:
  186. die(2,'Daemon wait timeout for {} {} exceeded'.format(self.coin,self.network))
  187. @property
  188. def is_ready(self):
  189. return self.state == 'ready'
  190. @classmethod
  191. def check_implement(cls):
  192. m = 'required method {}() missing in class {}'
  193. for subcls in cls.__subclasses__():
  194. for k in cls.subclasses_must_implement:
  195. assert k in subcls.__dict__, m.format(k,subcls.__name__)
  196. class BitcoinDaemon(Daemon):
  197. cfg_file_hdr = '# BitcoinDaemon config file\n'
  198. def subclass_init(self):
  199. if self.platform == 'win' and self.coinsym == 'bch':
  200. self.use_pidfile = False
  201. if self.network=='testnet':
  202. self.testnet_arg = ['--testnet']
  203. self.shared_args = [
  204. '--datadir={}'.format(self.datadir),
  205. '--rpcport={}'.format(self.rpc_port) ]
  206. self.coind_args = [
  207. '--listen=0',
  208. '--keypool=1',
  209. '--rpcallowip=127.0.0.1',
  210. '--rpcbind=127.0.0.1:{}'.format(self.rpc_port) ]
  211. if self.use_pidfile:
  212. self.coind_args += ['--pid='+self.pidfile]
  213. if self.platform == 'linux':
  214. self.coind_args += ['--daemon']
  215. if self.coinsym == 'bch':
  216. self.coin_specific_coind_args = ['--usecashaddr=0']
  217. elif self.coinsym == 'ltc':
  218. self.coin_specific_coind_args = ['--mempoolreplacement=1']
  219. @property
  220. def state(self):
  221. cp = self.cli('getblockcount',silent=True,check=False)
  222. err = cp.stderr.decode()
  223. if ("error: couldn't connect" in err
  224. or "error: Could not connect" in err
  225. or "does not exist" in err ):
  226. return 'stopped'
  227. elif cp.returncode == 0:
  228. return 'ready'
  229. else:
  230. return 'busy'
  231. @property
  232. def stop_cmd(self):
  233. return self.cli_cmd('stop')
  234. class MoneroDaemon(Daemon):
  235. @property
  236. def shared_args(self):
  237. return ['--zmq-rpc-bind-port={}'.format(self.rpc_port+1),'--rpc-bind-port={}'.format(self.rpc_port)]
  238. @property
  239. def coind_args(self):
  240. return ['--bg-mining-enable',
  241. '--pidfile={}'.format(self.pidfile),
  242. '--data-dir={}'.format(self.datadir),
  243. '--detach',
  244. '--offline' ]
  245. @property
  246. def state(self):
  247. cp = self.run_cmd(
  248. [self.coind_exec]
  249. + self.shared_args
  250. + ['status'],
  251. silent=True,
  252. check=False )
  253. return 'stopped' if 'Error:' in cp.stdout.decode() else 'ready'
  254. @property
  255. def stop_cmd(self):
  256. return [self.coind_exec] + self.shared_args + ['exit']
  257. class EthereumDaemon(Daemon):
  258. def subclass_init(self):
  259. # defaults:
  260. # linux: $HOME/.local/share/io.parity.ethereum/chains/DevelopmentChain
  261. # win: $LOCALAPPDATA/Parity/Ethereum/chains/DevelopmentChain
  262. self.chaindir = os.path.join(self.datadir,'devchain')
  263. shutil.rmtree(self.chaindir,ignore_errors=True)
  264. @property
  265. def coind_cmd(self):
  266. return ['daemon',self.pidfile] if self.platform == 'linux' else []
  267. @property
  268. def coind_args(self):
  269. return ['--ports-shift={}'.format(self.port_shift),
  270. '--base-path={}'.format(self.chaindir),
  271. '--config=dev',
  272. '--log-file={}'.format(os.path.join(self.datadir,'parity.log')) ]
  273. @property
  274. def state(self):
  275. from mmgen.rpc import EthereumRPCConnection
  276. try:
  277. conn = EthereumRPCConnection('localhost',self.rpc_port,socket_timeout=0.2)
  278. except:
  279. return 'stopped'
  280. ret = conn.eth_chainId(on_fail='return')
  281. return ('stopped','ready')[ret == '0x11']
  282. @property
  283. def stop_cmd(self):
  284. return ['kill','-Wf',self.pid] if g.platform == 'win' else ['kill',self.pid]
  285. @property
  286. def pid(self): # TODO: distinguish between ETH and ETC
  287. if g.platform == 'win':
  288. cp = self.run_cmd(['ps','-Wl'],silent=True,check=False)
  289. for line in cp.stdout.decode().splitlines():
  290. if 'parity.exe' in line:
  291. return line.split()[3] # use Windows, not Cygwin, PID
  292. else:
  293. return super().pid
  294. Daemon.check_implement()