#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2019-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import os import sys import json import shutil import signal import logging import traceback import subprocess import basicswap.config as cfg from basicswap import __version__ from basicswap.ui.util import getCoinName from basicswap.basicswap import BasicSwap from basicswap.chainparams import chainparams from basicswap.http_server import HttpThread from basicswap.contrib.websocket_server import WebsocketServer logger = logging.getLogger() logger.level = logging.DEBUG if not len(logger.handlers): logger.addHandler(logging.StreamHandler(sys.stdout)) swap_client = None class Daemon: __slots__ = ('handle', 'files') def __init__(self, handle, files): self.handle = handle self.files = files def is_known_coin(coin_name: str) -> bool: for k, v in chainparams.items(): if coin_name == v['name']: return True return False def signal_handler(sig, frame): global swap_client logger.info('Signal %d detected, ending program.' % (sig)) if swap_client is not None: swap_client.stopRunning() def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}): daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) datadir_path = os.path.expanduser(node_dir) # Rewrite litecoin.conf for 0.21.3 ltc_conf_path = os.path.join(datadir_path, 'litecoin.conf') if os.path.exists(ltc_conf_path): config_to_add = ['blockfilterindex=0', 'peerblockfilters=0'] with open(ltc_conf_path) as fp: for line in fp: line = line.strip() if line in config_to_add: config_to_add.remove(line) if len(config_to_add) > 0: logging.info('Rewriting litecoin.conf') shutil.copyfile(ltc_conf_path, ltc_conf_path + '.last') with open(ltc_conf_path, 'a') as fp: for line in config_to_add: fp.write(line + '\n') args = [daemon_bin, ] add_datadir: bool = extra_config.get('add_datadir', True) if add_datadir: args.append('-datadir=' + datadir_path) args += opts logging.info('Starting node ' + daemon_bin + ' ' + (('-datadir=' + node_dir) if add_datadir else '')) opened_files = [] if extra_config.get('stdout_to_file', False): stdout_dest = open(os.path.join(datadir_path, extra_config.get('stdout_filename', 'core_stdout.log')), 'w') opened_files.append(stdout_dest) stderr_dest = stdout_dest else: stdout_dest = subprocess.PIPE stderr_dest = subprocess.PIPE if extra_config.get('use_shell', False): str_args = ' '.join(args) return Daemon(subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files) return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files) def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]): daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) datadir_path = os.path.expanduser(node_dir) args = [daemon_bin, '--non-interactive', '--config-file=' + os.path.join(datadir_path, 'monerod.conf')] + opts logging.info('Starting node {} --data-dir={}'.format(daemon_bin, node_dir)) # return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) file_stdout = open(os.path.join(datadir_path, 'core_stdout.log'), 'w') file_stderr = open(os.path.join(datadir_path, 'core_stderr.log'), 'w') return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=file_stdout, stderr=file_stderr, cwd=datadir_path), [file_stdout, file_stderr]) def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]): daemon_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin)) args = [daemon_bin, '--non-interactive'] needs_rewrite: bool = False config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy='] data_dir = os.path.expanduser(node_dir) config_path = os.path.join(data_dir, 'monero_wallet.conf') if os.path.exists(config_path): args += ['--config-file=' + config_path] with open(config_path) as fp: for line in fp: if any(line.startswith(config_line) for config_line in config_to_remove): logging.warning('Found old config in monero_wallet.conf: {}'.format(line.strip())) needs_rewrite = True args += opts if needs_rewrite: logging.info('Rewriting monero_wallet.conf') shutil.copyfile(config_path, config_path + '.last') with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to: for line in fp_from: if not any(line.startswith(config_line) for config_line in config_to_remove): fp_to.write(line) logging.info('Starting wallet daemon {} --wallet-dir={}'.format(daemon_bin, node_dir)) # TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir) wallet_stdout = open(os.path.join(data_dir, 'wallet_stdout.log'), 'w') wallet_stderr = open(os.path.join(data_dir, 'wallet_stderr.log'), 'w') return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=wallet_stdout, stderr=wallet_stderr, cwd=data_dir), [wallet_stdout, wallet_stderr]) def ws_new_client(client, server): if swap_client: swap_client.log.debug(f'ws_new_client {client["id"]}') def ws_client_left(client, server): if client is None: return if swap_client: swap_client.log.debug(f'ws_client_left {client["id"]}') def ws_message_received(client, server, message): if len(message) > 200: message = message[:200] + '..' if swap_client: swap_client.log.debug(f'ws_message_received {client["id"]} {message}') def runClient(fp, data_dir, chain, start_only_coins): global swap_client daemons = [] pids = [] threads = [] settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME) pids_path = os.path.join(data_dir, '.pids') if os.getenv('WALLET_ENCRYPTION_PWD', '') != '': if 'decred' in start_only_coins: # Workaround for dcrwallet requiring password for initial startup logger.warning('Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred.') else: raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.') if not os.path.exists(settings_path): raise ValueError('Settings file not found: ' + str(settings_path)) with open(settings_path) as fs: settings = json.load(fs) swap_client = BasicSwap(fp, data_dir, settings, chain) if os.path.exists(pids_path): with open(pids_path) as fd: for ln in fd: # TODO: try close logger.warning('Found pid for daemon {} '.format(ln.strip())) # Ensure daemons are stopped swap_client.stopDaemons() # Settings may have been modified settings = swap_client.settings try: # Try start daemons for c, v in settings['chainclients'].items(): if len(start_only_coins) > 0 and c not in start_only_coins: continue try: coin_id = swap_client.getCoinIdFromName(c) display_name = getCoinName(coin_id) except Exception as e: logger.warning('Error getting coin display name for {}: {}'.format(c, str(e))) display_name = 'Unknown' if c == 'monero': if v['manage_daemon'] is True: swap_client.log.info(f'Starting {display_name} daemon') filename = 'monerod' + ('.exe' if os.name == 'nt' else '') daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename)) pid = daemons[-1].handle.pid swap_client.log.info('Started {} {}'.format(filename, pid)) if v['manage_wallet_daemon'] is True: swap_client.log.info(f'Starting {display_name} wallet daemon') daemon_addr = '{}:{}'.format(v['rpchost'], v['rpcport']) trusted_daemon: bool = swap_client.getXMRTrustedDaemon(coin_id, v['rpchost']) opts = ['--daemon-address', daemon_addr, ] proxy_log_str = '' proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost']) if proxy_host: proxy_log_str = ' through proxy' opts += ['--proxy', f'{proxy_host}:{proxy_port}', ] swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str)) daemon_rpcuser = v.get('rpcuser', '') daemon_rpcpass = v.get('rpcpassword', '') if daemon_rpcuser != '': opts.append('--daemon-login') opts.append(daemon_rpcuser + ':' + daemon_rpcpass) opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon') filename = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '') daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts)) pid = daemons[-1].handle.pid swap_client.log.info('Started {} {}'.format(filename, pid)) continue # /monero if c == 'decred': appdata = v['datadir'] extra_opts = [f'--appdata="{appdata}"', ] use_shell: bool = True if os.name == 'nt' else False if v['manage_daemon'] is True: swap_client.log.info(f'Starting {display_name} daemon') filename = 'dcrd' + ('.exe' if os.name == 'nt' else '') extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrd_stdout.log', 'use_shell': use_shell} daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config)) pid = daemons[-1].handle.pid swap_client.log.info('Started {} {}'.format(filename, pid)) if v['manage_wallet_daemon'] is True: swap_client.log.info(f'Starting {display_name} wallet daemon') filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '') wallet_pwd = v['wallet_pwd'] if wallet_pwd == '': # Only set when in startonlycoin mode wallet_pwd = os.getenv('WALLET_ENCRYPTION_PWD', '') if wallet_pwd != '': extra_opts.append(f'--pass="{wallet_pwd}"') extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrwallet_stdout.log', 'use_shell': use_shell} daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config)) pid = daemons[-1].handle.pid swap_client.log.info('Started {} {}'.format(filename, pid)) continue # /decred if v['manage_daemon'] is True: swap_client.log.info(f'Starting {display_name} daemon') filename = c + 'd' + ('.exe' if os.name == 'nt' else '') daemons.append(startDaemon(v['datadir'], v['bindir'], filename)) pid = daemons[-1].handle.pid pids.append((c, pid)) swap_client.setDaemonPID(c, pid) swap_client.log.info('Started {} {}'.format(filename, pid)) if len(pids) > 0: with open(pids_path, 'w') as fd: for p in pids: fd.write('{}:{}\n'.format(*p)) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) if len(start_only_coins) > 0: logger.info(f'Only running {start_only_coins}. Manually exit with Ctrl + c when ready.') while not swap_client.delay_event.wait(0.5): pass else: swap_client.start() if 'htmlhost' in settings: swap_client.log.info('Starting http server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport'])) allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS thread_http = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client) threads.append(thread_http) thread_http.start() if 'wshost' in settings: ws_url = 'ws://{}:{}'.format(settings['wshost'], settings['wsport']) swap_client.log.info(f'Starting ws server at {ws_url}.') swap_client.ws_server = WebsocketServer(host=settings['wshost'], port=settings['wsport']) swap_client.ws_server.set_fn_new_client(ws_new_client) swap_client.ws_server.set_fn_client_left(ws_client_left) swap_client.ws_server.set_fn_message_received(ws_message_received) swap_client.ws_server.run_forever(threaded=True) logger.info('Exit with Ctrl + c.') while not swap_client.delay_event.wait(0.5): swap_client.update() except Exception as ex: traceback.print_exc() if swap_client.ws_server: try: swap_client.log.info('Stopping websocket server.') swap_client.ws_server.shutdown_gracefully() except Exception as ex: traceback.print_exc() swap_client.finalise() swap_client.log.info('Stopping HTTP threads.') for t in threads: try: t.stop() t.join() except Exception as ex: traceback.print_exc() closed_pids = [] for d in daemons: swap_client.log.info('Interrupting {}'.format(d.handle.pid)) try: d.handle.send_signal(signal.CTRL_C_EVENT if os.name == 'nt' else signal.SIGINT) except Exception as e: swap_client.log.info('Interrupting %d, error %s', d.handle.pid, str(e)) for d in daemons: try: d.handle.wait(timeout=120) for fp in [d.handle.stdout, d.handle.stderr, d.handle.stdin] + d.files: if fp: fp.close() closed_pids.append(d.handle.pid) except Exception as ex: swap_client.log.error('Error: {}'.format(ex)) if os.path.exists(pids_path): with open(pids_path) as fd: lines = fd.read().split('\n') still_running = '' for ln in lines: try: if not int(ln.split(':')[1]) in closed_pids: still_running += ln + '\n' except Exception: pass with open(pids_path, 'w') as fd: fd.write(still_running) def printVersion(): logger.info('Basicswap version: %s', __version__) def printHelp(): print('Usage: basicswap-run ') print('\n--help, -h Print help.') print('--version, -v Print version.') print('--datadir=PATH Path to basicswap data directory, default:{}.'.format(cfg.BASICSWAP_DATADIR)) print('--mainnet Run in mainnet mode.') print('--testnet Run in testnet mode.') print('--regtest Run in regtest mode.') print('--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing.') def main(): data_dir = None chain = 'mainnet' start_only_coins = set() for v in sys.argv[1:]: if len(v) < 2 or v[0] != '-': logger.warning('Unknown argument %s', v) continue s = v.split('=') name = s[0].strip() for i in range(2): if name[0] == '-': name = name[1:] if name == 'v' or name == 'version': printVersion() return 0 if name == 'h' or name == 'help': printHelp() return 0 if name in ('mainnet', 'testnet', 'regtest'): chain = name continue if len(s) == 2: if name == 'datadir': data_dir = os.path.expanduser(s[1]) continue if name == 'startonlycoin': for coin in [s.lower() for s in s[1].split(',')]: if is_known_coin(coin) is False: raise ValueError(f'Unknown coin: {coin}') start_only_coins.add(coin) continue logger.warning('Unknown argument %s', v) if os.name == 'nt': logger.warning('Running on windows is discouraged and windows support may be discontinued in the future. Please consider using the WSL docker setup instead.') if data_dir is None: data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR)) logger.info('Using datadir: %s', data_dir) logger.info('Chain: %s', chain) if not os.path.exists(data_dir): os.makedirs(data_dir) with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp: logger.info(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n') runClient(fp, data_dir, chain, start_only_coins) logger.info('Done.') return swap_client.fail_code if swap_client is not None else 0 if __name__ == '__main__': main()