#!/usr/bin/env python """ The PyBitmessage startup script """ # Copyright (c) 2012-2016 Jonathan Warren # Copyright (c) 2012-2022 The Bitmessage developers # Distributed under the MIT/X11 software license. See the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # Right now, PyBitmessage only support connecting to stream 1. It doesn't # yet contain logic to expand into further streams. import os import sys try: import pathmagic except ImportError: from pybitmessage import pathmagic app_dir = pathmagic.setup() import depends depends.check_dependencies() import getopt import multiprocessing # Used to capture a Ctrl-C keypress so that Bitmessage can shutdown gracefully. import signal import threading import time import traceback import defaults import shared import shutdown import state from testmode_init import populate_api_test_data from bmconfigparser import config from debug import logger # this should go before any threads from helper_startup import ( adjustHalfOpenConnectionsLimit, fixSocket, start_proxyconfig) from inventory import Inventory # Network objects and threads from network import ( BMConnectionPool, Dandelion, AddrThread, AnnounceThread, BMNetworkThread, InvThread, ReceiveQueueThread, DownloadThread, UploadThread ) from network.knownnodes import readKnownNodes from singleinstance import singleinstance # Synchronous threads from threads import ( set_thread_name, printLock, addressGenerator, objectProcessor, singleCleaner, singleWorker, sqlThread) def signal_handler(signum, frame): """Single handler for any signal sent to pybitmessage""" process = multiprocessing.current_process() thread = threading.current_thread() logger.error( 'Got signal %i in %s/%s', signum, process.name, thread.name ) if process.name == "RegExParser": # on Windows this isn't triggered, but it's fine, # it has its own process termination thing raise SystemExit if "PoolWorker" in process.name: raise SystemExit if thread.name not in ("PyBitmessage", "MainThread"): return logger.error("Got signal %i", signum) # there are possible non-UI variants to run bitmessage # which should shutdown especially test-mode if state.thisapp.daemon or not state.enableGUI: shutdown.doCleanShutdown() else: print('# Thread: %s(%d)' % (thread.name, thread.ident)) for filename, lineno, name, line in traceback.extract_stack(frame): print('File: "%s", line %d, in %s' % (filename, lineno, name)) if line: print(' %s' % line.strip()) print('Unfortunately you cannot use Ctrl+C when running the UI' ' because the UI captures the signal.') class Main(object): """Main PyBitmessage class""" def start(self): """Start main application""" # pylint: disable=too-many-statements,too-many-branches,too-many-locals fixSocket() adjustHalfOpenConnectionsLimit() daemon = config.safeGetBoolean('bitmessagesettings', 'daemon') try: opts, _ = getopt.getopt( sys.argv[1:], "hcdt", ["help", "curses", "daemon", "test"]) except getopt.GetoptError: self.usage() sys.exit(2) for opt, _ in opts: if opt in ("-h", "--help"): self.usage() sys.exit() elif opt in ("-d", "--daemon"): daemon = True elif opt in ("-c", "--curses"): state.curses = True elif opt in ("-t", "--test"): state.testmode = True if os.path.isfile(os.path.join( state.appdata, 'unittest.lock')): daemon = True state.enableGUI = False # run without a UI # Fallback: in case when no api command was issued state.last_api_response = time.time() # Apply special settings config.set( 'bitmessagesettings', 'apienabled', 'true') config.set( 'bitmessagesettings', 'apiusername', 'username') config.set( 'bitmessagesettings', 'apipassword', 'password') config.set( 'bitmessagesettings', 'apivariant', 'legacy') config.set( 'bitmessagesettings', 'apinotifypath', os.path.join(app_dir, 'tests', 'apinotify_handler.py') ) if daemon: state.enableGUI = False # run without a UI if state.enableGUI and not state.curses and not depends.check_pyqt(): sys.exit( 'PyBitmessage requires PyQt unless you want' ' to run it as a daemon and interact with it' ' using the API. You can download PyQt from ' 'http://www.riverbankcomputing.com/software/pyqt/download' ' or by searching Google for \'PyQt Download\'.' ' If you want to run in daemon mode, see ' 'https://bitmessage.org/wiki/Daemon\n' 'You can also run PyBitmessage with' ' the new curses interface by providing' ' \'-c\' as a commandline argument.' ) # is the application already running? If yes then exit. state.thisapp = singleinstance("", daemon) if daemon: with printLock: print('Running as a daemon. Send TERM signal to end.') self.daemonize() self.setSignalHandler() set_thread_name("PyBitmessage") state.dandelion = config.safeGetInt('network', 'dandelion') # dandelion requires outbound connections, without them, # stem objects will get stuck forever if state.dandelion and not config.safeGetBoolean( 'bitmessagesettings', 'sendoutgoingconnections'): state.dandelion = 0 if state.testmode or config.safeGetBoolean( 'bitmessagesettings', 'extralowdifficulty'): defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int( defaults.networkDefaultProofOfWorkNonceTrialsPerByte / 100) defaults.networkDefaultPayloadLengthExtraBytes = int( defaults.networkDefaultPayloadLengthExtraBytes / 100) readKnownNodes() # Not needed if objproc is disabled if state.enableObjProc: # Start the address generation thread addressGeneratorThread = addressGenerator() # close the main program even if there are threads left addressGeneratorThread.daemon = True addressGeneratorThread.start() # Start the thread that calculates POWs singleWorkerThread = singleWorker() # close the main program even if there are threads left singleWorkerThread.daemon = True singleWorkerThread.start() # Start the SQL thread sqlLookup = sqlThread() # DON'T close the main program even if there are threads left. # The closeEvent should command this thread to exit gracefully. sqlLookup.daemon = False sqlLookup.start() Inventory() # init # init, needs to be early because other thread may access it early Dandelion() # Enable object processor and SMTP only if objproc enabled if state.enableObjProc: # SMTP delivery thread if daemon and config.safeGet( 'bitmessagesettings', 'smtpdeliver', '') != '': from class_smtpDeliver import smtpDeliver smtpDeliveryThread = smtpDeliver() smtpDeliveryThread.start() # SMTP daemon thread if daemon and config.safeGetBoolean( 'bitmessagesettings', 'smtpd'): from class_smtpServer import smtpServer smtpServerThread = smtpServer() smtpServerThread.start() # Start the thread that calculates POWs objectProcessorThread = objectProcessor() # DON'T close the main program even the thread remains. # This thread checks the shutdown variable after processing # each object. objectProcessorThread.daemon = False objectProcessorThread.start() # Start the cleanerThread singleCleanerThread = singleCleaner() # close the main program even if there are threads left singleCleanerThread.daemon = True singleCleanerThread.start() # Not needed if objproc disabled if state.enableObjProc: shared.reloadMyAddressHashes() shared.reloadBroadcastSendersForWhichImWatching() # API is also objproc dependent if config.safeGetBoolean('bitmessagesettings', 'apienabled'): import api # pylint: disable=relative-import singleAPIThread = api.singleAPI() # close the main program even if there are threads left singleAPIThread.daemon = True singleAPIThread.start() # start network components if networking is enabled if state.enableNetwork: start_proxyconfig() BMConnectionPool().connectToStream(1) asyncoreThread = BMNetworkThread() asyncoreThread.daemon = True asyncoreThread.start() for i in range(config.getint('threads', 'receive')): receiveQueueThread = ReceiveQueueThread(i) receiveQueueThread.daemon = True receiveQueueThread.start() if config.safeGetBoolean('bitmessagesettings', 'udp'): state.announceThread = AnnounceThread() state.announceThread.daemon = True state.announceThread.start() state.invThread = InvThread() state.invThread.daemon = True state.invThread.start() state.addrThread = AddrThread() state.addrThread.daemon = True state.addrThread.start() state.downloadThread = DownloadThread() state.downloadThread.daemon = True state.downloadThread.start() state.uploadThread = UploadThread() state.uploadThread.daemon = True state.uploadThread.start() if config.safeGetBoolean('bitmessagesettings', 'upnp'): import upnp upnpThread = upnp.uPnPThread() upnpThread.start() else: # Populate with hardcoded value (same as connectToStream above) state.streamsInWhichIAmParticipating.append(1) if not daemon and state.enableGUI: if state.curses: if not depends.check_curses(): sys.exit() print('Running with curses') import bitmessagecurses bitmessagecurses.runwrapper() else: import bitmessageqt bitmessageqt.run() else: config.remove_option('bitmessagesettings', 'dontconnect') if state.testmode: populate_api_test_data() if daemon: while state.shutdown == 0: time.sleep(1) if ( state.testmode and time.time() - state.last_api_response >= 30 ): self.stop() elif not state.enableGUI: state.enableGUI = True try: # pylint: disable=relative-import from tests import core as test_core except ImportError: self.stop() return test_core_result = test_core.run() self.stop() test_core.cleanup() sys.exit(not test_core_result.wasSuccessful()) @staticmethod def daemonize(): """Running as a daemon. Send signal in end.""" grandfatherPid = os.getpid() parentPid = None try: if os.fork(): # unlock state.thisapp.cleanup() # wait until grandchild ready while True: time.sleep(1) os._exit(0) # pylint: disable=protected-access except AttributeError: # fork not implemented pass else: parentPid = os.getpid() state.thisapp.lock() # relock os.umask(0) try: os.setsid() except AttributeError: # setsid not implemented pass try: if os.fork(): # unlock state.thisapp.cleanup() # wait until child ready while True: time.sleep(1) os._exit(0) # pylint: disable=protected-access except AttributeError: # fork not implemented pass else: state.thisapp.lock() # relock state.thisapp.lockPid = None # indicate we're the final child sys.stdout.flush() sys.stderr.flush() if not sys.platform.startswith('win'): si = open(os.devnull, 'r') so = open(os.devnull, 'a+') se = open(os.devnull, 'a+', 0) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) if parentPid: # signal ready os.kill(parentPid, signal.SIGTERM) os.kill(grandfatherPid, signal.SIGTERM) @staticmethod def setSignalHandler(): """Setting the Signal Handler""" signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) # signal.signal(signal.SIGINT, signal.SIG_DFL) @staticmethod def usage(): """Displaying the usages""" print('Usage: ' + sys.argv[0] + ' [OPTIONS]') print(''' Options: -h, --help show this help message and exit -c, --curses use curses (text mode) interface -d, --daemon run in daemon (background) mode -t, --test dryrun, make testing All parameters are optional. ''') @staticmethod def stop(): """Stop main application""" with printLock: print('Stopping Bitmessage Deamon.') shutdown.doCleanShutdown() # .. todo:: nice function but no one is using this @staticmethod def getApiAddress(): """This function returns API address and port""" if not config.safeGetBoolean( 'bitmessagesettings', 'apienabled'): return None address = config.get('bitmessagesettings', 'apiinterface') port = config.getint('bitmessagesettings', 'apiport') return {'address': address, 'port': port} def main(): """Triggers main module""" mainprogram = Main() mainprogram.start() if __name__ == "__main__": main() # So far, the creation of and management of the Bitmessage protocol and this # client is a one-man operation. Bitcoin tips are quite appreciated. # 1H5XaDA6fYENLbknwZyjiYXYPQaFjjLX2u