autosign.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. autosign: Auto-sign MMGen transactions, message files and XMR wallet output files
  12. """
  13. import sys,os,asyncio
  14. from pathlib import Path
  15. from subprocess import run,DEVNULL
  16. from .cfg import Config
  17. from .util import msg,msg_r,ymsg,rmsg,gmsg,bmsg,die,suf,fmt,fmt_list,async_run
  18. from .color import yellow,red,orange
  19. from .wallet import Wallet,get_wallet_cls
  20. from .filename import find_file_in_dir
  21. from .ui import keypress_confirm
  22. class Signable:
  23. signables = ('transaction','message','xmr_transaction','xmr_wallet_outputs_file')
  24. class base:
  25. clean_all = False
  26. multiple_ok = True
  27. def __init__(self,parent):
  28. self.parent = parent
  29. self.cfg = parent.cfg
  30. self.dir = getattr(parent,self.dir_name)
  31. self.long_desc = (
  32. 'non-Monero transaction' if self.desc == 'transaction' and 'XMR' in self.parent.coins else
  33. self.desc)
  34. @property
  35. def unsigned(self):
  36. return self._unprocessed( '_unsigned', self.rawext, self.sigext )
  37. @property
  38. def unsubmitted(self):
  39. return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
  40. def _unprocessed(self,attrname,rawext,sigext):
  41. if not hasattr(self,attrname):
  42. dirlist = sorted(self.dir.iterdir())
  43. names = {f.name for f in dirlist}
  44. setattr(
  45. self,
  46. attrname,
  47. tuple(f for f in dirlist
  48. if f.name.endswith('.' + rawext)
  49. and f.name[:-len(rawext)] + sigext not in names) )
  50. return getattr(self,attrname)
  51. def print_bad_list(self,bad_files):
  52. msg('\n{a}\n{b}'.format(
  53. a = red(f'Failed {self.desc}s:'),
  54. b = ' {}\n'.format('\n '.join(self.gen_bad_list(sorted(bad_files,key=lambda f: f.name))))
  55. ))
  56. class transaction(base):
  57. desc = 'transaction'
  58. rawext = 'rawtx'
  59. sigext = 'sigtx'
  60. dir_name = 'tx_dir'
  61. fail_msg = 'failed to sign'
  62. async def sign(self,f):
  63. from .tx import UnsignedTX
  64. tx1 = UnsignedTX( cfg=self.cfg, filename=f )
  65. if tx1.proto.sign_mode == 'daemon':
  66. from .rpc import rpc_init
  67. tx1.rpc = await rpc_init( self.cfg, tx1.proto, ignore_wallet=True )
  68. from .tx.sign import txsign
  69. tx2 = await txsign( self.cfg, tx1, self.parent.wallet_files[:], None, None )
  70. if tx2:
  71. tx2.file.write(ask_write=False)
  72. return tx2
  73. else:
  74. return False
  75. def print_summary(self,signables):
  76. if self.cfg.full_summary:
  77. bmsg('\nAutosign summary:\n')
  78. msg_r('\n'.join(tx.info.format(terse=True) for tx in signables))
  79. return
  80. def gen():
  81. for tx in signables:
  82. non_mmgen = [o for o in tx.outputs if not o.mmid]
  83. if non_mmgen:
  84. yield (tx,non_mmgen)
  85. body = list(gen())
  86. if body:
  87. bmsg('\nAutosign summary:')
  88. fs = '{} {} {}'
  89. t_wid,a_wid = 6,44
  90. def gen():
  91. yield fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount')
  92. yield fs.format('-'*t_wid, '-'*a_wid, '-'*7)
  93. for tx,non_mmgen in body:
  94. for nm in non_mmgen:
  95. yield fs.format(
  96. tx.txid.fmt( width=t_wid, color=True ) if nm is non_mmgen[0] else ' '*t_wid,
  97. nm.addr.fmt( width=a_wid, color=True ),
  98. nm.amt.hl() + ' ' + yellow(tx.coin))
  99. msg('\n' + '\n'.join(gen()))
  100. else:
  101. msg('\nNo non-MMGen outputs')
  102. def gen_bad_list(self,bad_files):
  103. for f in bad_files:
  104. yield red(f.name)
  105. class xmr_signable(transaction): # mixin class
  106. def need_daemon_restart(self,m,new_idx):
  107. old_idx = self.parent.xmr_cur_wallet_idx
  108. self.parent.xmr_cur_wallet_idx = new_idx
  109. return old_idx != new_idx or m.wd.state != 'ready'
  110. def print_summary(self,signables):
  111. bmsg('\nAutosign summary:')
  112. msg('\n'.join(s.get_info(indent=' ') for s in signables) + self.summary_footer)
  113. class xmr_transaction(xmr_signable):
  114. dir_name = 'xmr_tx_dir'
  115. desc = 'Monero transaction'
  116. subext = 'subtx'
  117. multiple_ok = False
  118. summary_footer = ''
  119. async def sign(self,f):
  120. from .xmrwallet import MoneroMMGenTX,MoneroWalletOps,xmrwallet_uargs
  121. tx1 = MoneroMMGenTX.Completed( self.parent.xmrwallet_cfg, f )
  122. m = MoneroWalletOps.sign(
  123. self.parent.xmrwallet_cfg,
  124. xmrwallet_uargs(
  125. infile = str(self.parent.wallet_files[0]), # MMGen wallet file
  126. wallets = str(tx1.src_wallet_idx),
  127. spec = None ),
  128. )
  129. tx2 = await m.main( f, restart_daemon=self.need_daemon_restart(m,tx1.src_wallet_idx) )
  130. tx2.write(ask_write=False)
  131. return tx2
  132. class xmr_wallet_outputs_file(xmr_signable):
  133. desc = 'Monero wallet outputs file'
  134. rawext = 'raw'
  135. sigext = 'sig'
  136. dir_name = 'xmr_outputs_dir'
  137. clean_all = True
  138. summary_footer = '\n'
  139. async def sign(self,f):
  140. from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
  141. wallet_idx = MoneroWalletOps.wallet.get_idx_from_fn(f)
  142. m = MoneroWalletOps.export_key_images(
  143. self.parent.xmrwallet_cfg,
  144. xmrwallet_uargs(
  145. infile = str(self.parent.wallet_files[0]), # MMGen wallet file
  146. wallets = str(wallet_idx),
  147. spec = None ),
  148. )
  149. obj = await m.main( f, wallet_idx, restart_daemon=self.need_daemon_restart(m,wallet_idx) )
  150. obj.write()
  151. return obj
  152. class message(base):
  153. desc = 'message file'
  154. rawext = 'rawmsg.json'
  155. sigext = 'sigmsg.json'
  156. dir_name = 'msg_dir'
  157. fail_msg = 'failed to sign or signed incompletely'
  158. async def sign(self,f):
  159. from .msg import UnsignedMsg,SignedMsg
  160. m = UnsignedMsg( self.cfg, infile=f )
  161. await m.sign( wallet_files=self.parent.wallet_files[:] )
  162. m = SignedMsg( self.cfg, data=m.__dict__ )
  163. m.write_to_file(
  164. outdir = self.dir.resolve(),
  165. ask_overwrite = False )
  166. if m.data.get('failed_sids'):
  167. die('MsgFileFailedSID',f'Failed Seed IDs: {fmt_list(m.data["failed_sids"],fmt="bare")}')
  168. return m
  169. def print_summary(self,signables):
  170. gmsg('\nSigned message files:')
  171. for message in signables:
  172. gmsg(' ' + message.signed_filename)
  173. def gen_bad_list(self,bad_files):
  174. for f in bad_files:
  175. sigfile = f.parent / ( f.name[:-len(self.rawext)] + self.sigext )
  176. yield orange(sigfile.name) if sigfile.exists() else red(f.name)
  177. class Autosign:
  178. dfl_mountpoint = '/mnt/mmgen_autosign'
  179. dfl_wallet_dir = '/dev/shm/autosign'
  180. old_dfl_mountpoint = '/mnt/tx'
  181. dfl_dev_label_dir = '/dev/disk/by-label'
  182. dev_label = 'MMGEN_TX'
  183. old_dfl_mountpoint_errmsg = f"""
  184. Mountpoint '{old_dfl_mountpoint}' is no longer supported!
  185. Please rename '{old_dfl_mountpoint}' to '{dfl_mountpoint}'
  186. and update your fstab accordingly.
  187. """
  188. mountpoint_errmsg_fs = """
  189. Mountpoint '{}' does not exist or does not point
  190. to a directory! Please create the mountpoint and add an entry
  191. to your fstab as described in this script’s help text.
  192. """
  193. mn_fmts = {
  194. 'mmgen': 'words',
  195. 'bip39': 'bip39',
  196. }
  197. dfl_mn_fmt = 'mmgen'
  198. have_msg_dir = False
  199. def __init__(self,cfg):
  200. self.cfg = cfg
  201. if cfg.mnemonic_fmt:
  202. if cfg.mnemonic_fmt not in self.mn_fmts:
  203. die(1,'{!r}: invalid mnemonic format (must be one of: {})'.format(
  204. cfg.mnemonic_fmt,
  205. fmt_list( self.mn_fmts, fmt='no_spc' ) ))
  206. if pfx := cfg.test_suite_root_pfx:
  207. subdir = 'online' if cfg.online else 'offline'
  208. self.mountpoint = Path(f'{pfx}/{subdir}/{self.dfl_mountpoint}')
  209. self.wallet_dir = Path(f'{pfx}/{subdir}/{self.dfl_wallet_dir}')
  210. self.dev_label_path = Path(f'{pfx}/{subdir}/{self.dfl_dev_label_dir}') / self.dev_label
  211. # mount --type=fuse-ext2 --options=rw+ ### current fuse-ext2 (0.4 29) is buggy - can’t use
  212. self.fs_image_path = Path(f'{pfx}/removable_device_image')
  213. self.mount_cmd = f'sudo mount {self.fs_image_path}'
  214. self.umount_cmd = 'sudo umount'
  215. else:
  216. self.mountpoint = Path(cfg.mountpoint or self.dfl_mountpoint)
  217. self.wallet_dir = Path(cfg.wallet_dir or self.dfl_wallet_dir)
  218. self.dev_label_path = Path(self.dfl_dev_label_dir) / self.dev_label
  219. self.mount_cmd = 'mount'
  220. self.umount_cmd = 'umount'
  221. self.tx_dir = self.mountpoint / 'tx'
  222. self.msg_dir = self.mountpoint / 'msg'
  223. self.keyfile = self.mountpoint / 'autosign.key'
  224. if any(k in cfg._uopts for k in ('help','longhelp')):
  225. return
  226. if 'coin' in cfg._uopts:
  227. die(1,'--coin option not supported with this command. Use --coins instead')
  228. self.coins = cfg.coins.upper().split(',') if cfg.coins else []
  229. if cfg.xmrwallets and not 'XMR' in self.coins:
  230. self.coins.append('XMR')
  231. if not self.coins:
  232. ymsg('Warning: no coins specified, defaulting to BTC')
  233. self.coins = ['BTC']
  234. if 'XMR' in self.coins:
  235. self.xmr_dir = self.mountpoint / 'xmr'
  236. self.xmr_tx_dir = self.mountpoint / 'xmr' / 'tx'
  237. self.xmr_outputs_dir = self.mountpoint / 'xmr' / 'outputs'
  238. self.xmr_cur_wallet_idx = None
  239. async def check_daemons_running(self):
  240. from .protocol import init_proto
  241. for coin in self.coins:
  242. proto = init_proto( self.cfg, coin, testnet=self.cfg.network=='testnet', need_amt=True )
  243. if proto.sign_mode == 'daemon':
  244. self.cfg._util.vmsg(f'Checking {coin} daemon')
  245. from .rpc import rpc_init
  246. from .exception import SocketError
  247. try:
  248. await rpc_init( self.cfg, proto, ignore_wallet=True )
  249. except SocketError as e:
  250. from .daemon import CoinDaemon
  251. d = CoinDaemon( self.cfg, proto=proto, test_suite=self.cfg.test_suite )
  252. die(2,
  253. f'\n{e}\nIs the {d.coind_name} daemon ({d.exec_fn}) running '
  254. + 'and listening on the correct port?' )
  255. @property
  256. def wallet_files(self):
  257. if not hasattr(self,'_wallet_files'):
  258. try:
  259. dirlist = self.wallet_dir.iterdir()
  260. except:
  261. die(1,f"Cannot open wallet directory '{self.wallet_dir}'. Did you run ‘mmgen-autosign setup’?")
  262. self._wallet_files = [f for f in dirlist if f.suffix == '.mmdat']
  263. if not self._wallet_files:
  264. die(1,'No wallet files present!')
  265. return self._wallet_files
  266. def do_mount(self,silent=False,no_dir_chk=False,no_xmr_chk=False):
  267. from stat import S_ISDIR,S_IWUSR,S_IRUSR
  268. def check_dir(cdir):
  269. try:
  270. ds = cdir.stat()
  271. assert S_ISDIR(ds.st_mode), f"'{cdir}' is not a directory!"
  272. assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR, f"'{cdir}' is not read/write for this user!"
  273. except:
  274. die(1,f"'{cdir}' missing or not read/writable by user!")
  275. if not self.mountpoint.is_dir():
  276. def do_die(m):
  277. die(1,'\n' + yellow(fmt(m.strip(),indent=' ')))
  278. if Path(self.old_dfl_mountpoint).is_dir():
  279. do_die(self.old_dfl_mountpoint_errmsg)
  280. else:
  281. do_die(self.mountpoint_errmsg_fs.format(self.mountpoint))
  282. if not self.mountpoint.is_mount():
  283. if run(
  284. self.mount_cmd.split() + [str(self.mountpoint)],
  285. stderr = DEVNULL,
  286. stdout = DEVNULL).returncode == 0:
  287. if not silent:
  288. msg(f"Mounting '{self.mountpoint}'")
  289. else:
  290. die(1,f"Unable to mount device at '{self.mountpoint}'")
  291. self.have_msg_dir = self.msg_dir.is_dir()
  292. if no_dir_chk:
  293. return
  294. check_dir(self.tx_dir)
  295. if self.have_msg_dir:
  296. check_dir(self.msg_dir)
  297. if 'XMR' in self.coins and not no_xmr_chk:
  298. check_dir(self.xmr_tx_dir)
  299. def do_umount(self,silent=False):
  300. if self.mountpoint.is_mount():
  301. run( ['sync'], check=True )
  302. if not silent:
  303. msg(f"Unmounting '{self.mountpoint}'")
  304. run(self.umount_cmd.split() + [str(self.mountpoint)], check=True)
  305. if not silent:
  306. bmsg('It is now safe to extract the removable device')
  307. def decrypt_wallets(self):
  308. msg(f"Unlocking wallet{suf(self.wallet_files)} with key from '{self.cfg.passwd_file}'")
  309. fails = 0
  310. for wf in self.wallet_files:
  311. try:
  312. Wallet( self.cfg, wf, ignore_in_fmt=True )
  313. except SystemExit as e:
  314. if e.code != 0:
  315. fails += 1
  316. return not fails
  317. async def sign_all(self,target_name):
  318. target = getattr(Signable,target_name)(self)
  319. if target.unsigned:
  320. good = []
  321. bad = []
  322. if len(target.unsigned) > 1 and not target.multiple_ok:
  323. ymsg(f'Autosign error: only one unsigned {target.desc} transaction allowed at a time!')
  324. target.print_bad_list(target.unsigned)
  325. return False
  326. for f in target.unsigned:
  327. ret = None
  328. try:
  329. ret = await target.sign(f)
  330. except Exception as e:
  331. ymsg(f"An error occurred with {target.desc} '{f.name}':\n {type(e).__name__}: {e!s}")
  332. except:
  333. ymsg(f"An error occurred with {target.desc} '{f.name}'")
  334. good.append(ret) if ret else bad.append(f)
  335. self.cfg._util.qmsg('')
  336. await asyncio.sleep(0.3)
  337. msg(f'{len(good)} {target.desc}{suf(good)} signed')
  338. if bad:
  339. rmsg(f'{len(bad)} {target.desc}{suf(bad)} {target.fail_msg}')
  340. if good and not self.cfg.no_summary:
  341. target.print_summary(good)
  342. if bad:
  343. target.print_bad_list(bad)
  344. return not bad
  345. else:
  346. msg(f'No unsigned {target.long_desc}s')
  347. await asyncio.sleep(0.5)
  348. return True
  349. def update_cfg(self):
  350. if not hasattr(self,'_cfg_updated'):
  351. self.cfg = Config({
  352. '_clone': self.cfg,
  353. 'outdir': str(self.tx_dir),
  354. 'passwd_file': str(self.keyfile),
  355. })
  356. self._cfg_updated = True
  357. async def do_sign(self):
  358. if not self.cfg.stealth_led:
  359. self.led.set('busy')
  360. self.do_mount()
  361. self.update_cfg()
  362. key_ok = self.decrypt_wallets()
  363. if key_ok:
  364. if self.cfg.stealth_led:
  365. self.led.set('busy')
  366. ret1 = await self.sign_all('transaction')
  367. ret2 = await self.sign_all('message') if self.have_msg_dir else True
  368. # import XMR wallet outputs BEFORE signing transactions:
  369. ret3 = await self.sign_all('xmr_wallet_outputs_file') if 'XMR' in self.coins else True
  370. ret4 = await self.sign_all('xmr_transaction') if 'XMR' in self.coins else True
  371. ret = ret1 and ret2 and ret3 and ret4
  372. self.do_umount()
  373. self.led.set(('standby','off','error')[(not ret)*2 or bool(self.cfg.stealth_led)])
  374. return ret
  375. else:
  376. msg('Password is incorrect!')
  377. self.do_umount()
  378. if not self.cfg.stealth_led:
  379. self.led.set('error')
  380. return False
  381. def wipe_existing_key(self):
  382. try:
  383. self.keyfile.stat()
  384. except:
  385. pass
  386. else:
  387. from .fileutil import shred_file
  388. msg(f"\nShredding existing key '{self.keyfile}'")
  389. shred_file( self.keyfile, verbose=self.cfg.verbose )
  390. def create_key(self):
  391. desc = f"key file '{self.keyfile}'"
  392. msg('Creating ' + desc)
  393. try:
  394. self.keyfile.write_text( os.urandom(32).hex() )
  395. self.keyfile.chmod(0o400)
  396. except:
  397. die(2,'Unable to write ' + desc)
  398. msg('Wrote ' + desc)
  399. def gen_key(self,no_unmount=False):
  400. if not self.get_insert_status():
  401. die(1,'Removable device not present!')
  402. self.do_mount(no_xmr_chk=True)
  403. self.wipe_existing_key()
  404. self.create_key()
  405. if not no_unmount:
  406. self.do_umount()
  407. def setup(self):
  408. def remove_wallet_dir():
  409. msg(f"Deleting '{self.wallet_dir}'")
  410. import shutil
  411. try:
  412. shutil.rmtree(self.wallet_dir)
  413. except:
  414. pass
  415. def create_wallet_dir():
  416. try:
  417. self.wallet_dir.mkdir(parents=True)
  418. except:
  419. pass
  420. try:
  421. self.wallet_dir.stat()
  422. except:
  423. die(2,f"Unable to create wallet directory '{self.wallet_dir}'")
  424. remove_wallet_dir()
  425. create_wallet_dir()
  426. self.gen_key(no_unmount=True)
  427. wf = find_file_in_dir( get_wallet_cls('mmgen'), self.cfg.data_dir )
  428. if wf and keypress_confirm(
  429. cfg = self.cfg,
  430. prompt = f"Default wallet '{wf}' found.\nUse default wallet for autosigning?",
  431. default_yes = True ):
  432. ss_in = Wallet( Config(), wf )
  433. else:
  434. ss_in = Wallet( self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt] )
  435. ss_out = Wallet( self.cfg, ss=ss_in, passwd_file=str(self.keyfile) )
  436. ss_out.write_to_file( desc='autosign wallet', outdir=self.wallet_dir )
  437. @property
  438. def xmrwallet_cfg(self):
  439. if not hasattr(self,'_xmrwallet_cfg'):
  440. self._xmrwallet_cfg = Config({
  441. '_clone': self.cfg,
  442. 'coin': 'xmr',
  443. 'wallet_rpc_user': 'autosign',
  444. 'wallet_rpc_password': 'autosign password',
  445. 'wallet_rpc_port': 23232 if self.cfg.test_suite_xmr_autosign else None,
  446. 'wallet_dir': str(self.wallet_dir),
  447. 'autosign': True,
  448. 'autosign_mountpoint': str(self.mountpoint),
  449. 'outdir': str(self.xmr_dir), # required by vkal.write()
  450. 'offline': True,
  451. })
  452. return self._xmrwallet_cfg
  453. def xmr_setup(self):
  454. def create_signing_wallets():
  455. from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
  456. if len(self.wallet_files) > 1:
  457. ymsg(f'Warning: more than one wallet file, using the first ({self.wallet_files[0]}) for xmrwallet generation')
  458. m = MoneroWalletOps.create_offline(
  459. self.xmrwallet_cfg,
  460. xmrwallet_uargs(
  461. infile = str(self.wallet_files[0]), # MMGen wallet file
  462. wallets = self.cfg.xmrwallets, # XMR wallet idxs
  463. spec = None ),
  464. )
  465. async_run(m.main())
  466. async_run(m.stop_wallet_daemon())
  467. import shutil
  468. try:
  469. shutil.rmtree(self.xmr_outputs_dir)
  470. except:
  471. pass
  472. self.update_cfg()
  473. self.xmr_outputs_dir.mkdir(parents=True)
  474. self.xmr_tx_dir.mkdir(exist_ok=True)
  475. self.clean_old_files()
  476. create_signing_wallets()
  477. def clean_old_files(self):
  478. def do_shred(f):
  479. nonlocal count
  480. msg_r('.')
  481. from .fileutil import shred_file
  482. shred_file( f, verbose=self.cfg.verbose )
  483. count += 1
  484. def clean_dir(s_name):
  485. def clean_files(rawext,sigext):
  486. for f in s.dir.iterdir():
  487. if s.clean_all and (f.name.endswith(f'.{rawext}') or f.name.endswith(f'.{sigext}')):
  488. do_shred(f)
  489. elif f.name.endswith(f'.{sigext}'):
  490. raw = f.parent / ( f.name[:-len(sigext)] + rawext )
  491. if raw.is_file():
  492. do_shred(raw)
  493. s = getattr(Signable,s_name)(self)
  494. msg_r(f"Cleaning directory '{s.dir}'..")
  495. if s.dir.is_dir():
  496. clean_files( s.rawext, s.sigext )
  497. if hasattr(s,'subext'):
  498. clean_files( s.rawext, s.subext )
  499. clean_files( s.sigext, s.subext )
  500. msg('done' if s.dir.is_dir() else 'skipped (no dir)')
  501. count = 0
  502. for s_name in Signable.signables:
  503. clean_dir(s_name)
  504. bmsg(f'{count} file{suf(count)} shredded')
  505. def get_insert_status(self):
  506. return self.cfg.no_insert_check or self.dev_label_path.exists()
  507. async def main_loop(self):
  508. if not self.cfg.stealth_led:
  509. self.led.set('standby')
  510. silent = self.cfg.test_suite_xmr_autosign
  511. n = 1 if silent else 0
  512. prev_status = False
  513. while True:
  514. status = self.get_insert_status()
  515. if status and not prev_status:
  516. msg('Device insertion detected')
  517. await self.do_sign()
  518. prev_status = status
  519. if not n % 10:
  520. msg_r(f"\r{' '*17}\rWaiting")
  521. await asyncio.sleep(1)
  522. if not silent:
  523. msg_r('.')
  524. n += 1
  525. def at_exit(self,exit_val,message=None):
  526. if message:
  527. msg(message)
  528. self.led.stop()
  529. sys.exit(0 if self.cfg.test_suite_xmr_autosign else int(exit_val))
  530. def init_exit_handler(self):
  531. def handler(arg1,arg2):
  532. self.at_exit(1,'\nCleaning up...')
  533. import signal
  534. signal.signal( signal.SIGTERM, handler )
  535. signal.signal( signal.SIGINT, handler )
  536. def init_led(self):
  537. from .led import LEDControl
  538. self.led = LEDControl(
  539. enabled = self.cfg.led,
  540. simulate = self.cfg.test_suite_autosign_led_simulate )
  541. self.led.set('off')