#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2019-2024 tecnovert # Copyright (c) 2024 The Basicswap developers # 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 # TODO: Remove 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: logger.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 logger.info("Starting node {}".format(daemon_bin)) logger.debug("Arguments {}".format(" ".join(args))) 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 shell: bool = False if extra_config.get("use_shell", False): args = " ".join(args) shell = True return Daemon( subprocess.Popen( args, shell=shell, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path, ), opened_files, ) def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]): daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) datadir_path = os.path.expanduser(node_dir) config_filename = ( "wownerod.conf" if daemon_bin.startswith("wow") else "monerod.conf" ) args = [ daemon_path, "--non-interactive", "--config-file=" + os.path.join(datadir_path, config_filename), ] + opts logger.info("Starting node {}".format(daemon_bin)) logger.debug("Arguments {}".format(" ".join(args))) # 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_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin)) args = [daemon_path, "--non-interactive"] needs_rewrite: bool = False config_to_remove = [ "daemon-address=", "untrusted-daemon=", "trusted-daemon=", "proxy=", ] data_dir = os.path.expanduser(node_dir) wallet_config_filename = ( "wownero-wallet-rpc.conf" if wallet_bin.startswith("wow") else "monero_wallet.conf" ) config_path = os.path.join(data_dir, wallet_config_filename) 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 ): logger.warning( "Found old config in monero_wallet.conf: {}".format( line.strip() ) ) needs_rewrite = True args += opts if needs_rewrite: logger.info("Rewriting wallet config") 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) logger.info("Starting wallet daemon {}".format(wallet_bin)) logger.debug("Arguments {}".format(" ".join(args))) # 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 getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str: return coin_settings.get( "core_binname", chainparams[coin_id].get("core_binname", default_name) ) + (".exe" if os.name == "nt" else "") def getWalletBinName(coin_id: int, coin_settings, default_name: str) -> str: return coin_settings.get( "wallet_binname", chainparams[coin_id].get("wallet_binname", default_name) ) + (".exe" if os.name == "nt" else "") def getCoreBinArgs(coin_id: int, coin_settings): extra_args = [] if "config_filename" in coin_settings: extra_args.append("--conf=" + coin_settings["config_filename"]) if "port" in coin_settings: extra_args.append("--port=" + str(int(coin_settings["port"]))) return extra_args def runClient(fp, data_dir, chain, start_only_coins): global swap_client, logger 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) logger = swap_client.log 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: # noqa: F841 logger.warning("Not starting unknown coin: {}".format(c)) continue if c in ("monero", "wownero"): if v["manage_daemon"] is True: swap_client.log.info(f"Starting {display_name} daemon") filename: str = getCoreBinName(coin_id, v, c + "d") 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}", "--daemon-ssl-allow-any-cert", ] 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: str = getWalletBinName(coin_id, v, c + "-wallet-rpc") 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: str = getCoreBinName(coin_id, v, "dcrd") 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: str = getWalletBinName(coin_id, v, "dcrwallet") 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: str = getCoreBinName(coin_id, v, c + "d") extra_opts = getCoreBinArgs(coin_id, v) daemons.append( startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts) ) 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) signal.signal(signal.SIGHUP, 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.client_port = settings.get( "wsclientport", 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 e: # noqa: F841 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 e: # noqa: F841 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 e: # noqa: F841 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 int(ln.split(":")[1]) not 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) print("Done.") return swap_client.fail_code if swap_client is not None else 0 if __name__ == "__main__": main()