mirror of
https://github.com/basicswap/basicswap.git
synced 2024-11-16 15:58:17 +00:00
447 lines
18 KiB
Python
Executable file
447 lines
18 KiB
Python
Executable file
#!/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('Not starting unknown coin: {}'.format(c))
|
|
continue
|
|
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()
|