#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2025 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 select import sqlite3 import subprocess import time from basicswap.util.daemon import Daemon def serverExistsInDatabase(simplex_db_path: str, server_address: str, logger) -> bool: try: # Extract hostname from SMP URL format: smp://fingerprint@hostname if server_address.startswith("smp://") and "@" in server_address: host = server_address.split("@")[-1] elif ":" in server_address: host = server_address.split(":", 1)[0] else: host = server_address with sqlite3.connect(simplex_db_path) as con: c = con.cursor() # Check for any server entry with this hostname query = ( "SELECT COUNT(*) FROM protocol_servers WHERE host LIKE ? OR host = ?" ) host_pattern = f"%{host}%" count = c.execute(query, (host_pattern, host)).fetchone()[0] if count > 0: logger.debug( f"Server {host} already exists in database ({count} entries)" ) return True else: logger.debug(f"Server {host} not found in database") return False except Exception as e: logger.error(f"Database check failed: {e}") return False def initSimplexClient(args, logger, delay_event): # Need to set initial profile through CLI # TODO: Must be a better way? logger.info("Initialising Simplex client") (pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read if os.name == "nt": str_args = " ".join(args) p = subprocess.Popen( str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w ) else: p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w) def readOutput(): buf = os.read(pipe_r, 1024).decode("utf-8") response = None # logger.debug(f"simplex-chat output: {buf}") if "display name:" in buf: logger.debug("Setting display name") response = b"user\n" else: logger.debug(f"Unexpected output: {buf}") return if response is not None: p.stdin.write(response) p.stdin.flush() try: start_time: int = time.time() max_wait_seconds: int = 60 while p.poll() is None: if time.time() > start_time + max_wait_seconds: raise RuntimeError("Timed out") if os.name == "nt": readOutput() delay_event.wait(0.1) continue while len(select.select([pipe_r], [], [], 0)[0]) == 1: readOutput() delay_event.wait(0.1) except Exception as e: logger.error(f"initSimplexClient: {e}") finally: if p.poll() is None: p.terminate() os.close(pipe_r) os.close(pipe_w) p.stdin.close() def startSimplexClient( bin_path: str, data_path: str, server_address: str, websocket_port: int, logger, delay_event, socks_proxy=None, log_level: str = "debug", ) -> Daemon: logger.info("Starting Simplex client") if not os.path.exists(data_path): os.makedirs(data_path) simplex_data_prefix = os.path.join(data_path, "simplex_client_data") simplex_db_path = simplex_data_prefix + "_chat.db" args = [bin_path, "-d", simplex_data_prefix, "-p", str(websocket_port)] if socks_proxy: args += ["--socks-proxy", socks_proxy] if not os.path.exists(simplex_db_path): # Database doesn't exist - safe to add server during initialization logger.info("Database not found, initializing Simplex client") init_args = args + ["-e", "/help"] # Run command to exit client init_args += ["-s", server_address] initSimplexClient(init_args, logger, delay_event) else: # Database exists - only add server if it's not already there if not serverExistsInDatabase(simplex_db_path, server_address, logger): logger.debug(f"Adding server to Simplex CLI args: {server_address}") args += ["-s", server_address] else: logger.debug("Server already exists, not adding to CLI args") args += ["-l", log_level] opened_files = [] stdout_dest = open( os.path.join(data_path, "simplex_stdout.log"), "w", ) opened_files.append(stdout_dest) stderr_dest = stdout_dest return Daemon( subprocess.Popen( args, shell=False, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=data_path, ), opened_files, "simplex-chat", )