384 lines
13 KiB
Python
Executable File
384 lines
13 KiB
Python
Executable File
#!/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
|
|
# Network subsystem
|
|
import network
|
|
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
|
|
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")
|
|
|
|
if state.testmode or config.safeGetBoolean(
|
|
'bitmessagesettings', 'extralowdifficulty'):
|
|
defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int(
|
|
defaults.networkDefaultProofOfWorkNonceTrialsPerByte / 100)
|
|
defaults.networkDefaultPayloadLengthExtraBytes = int(
|
|
defaults.networkDefaultPayloadLengthExtraBytes / 100)
|
|
|
|
# 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()
|
|
state.Inventory = Inventory() # init
|
|
|
|
if state.enableObjProc: # Not needed if objproc is disabled
|
|
# 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 object processing thread
|
|
objectProcessorThread = objectProcessor()
|
|
# DON'T close the main program even if the thread remains.
|
|
# This thread checks the shutdown variable after processing
|
|
# each object.
|
|
objectProcessorThread.daemon = False
|
|
objectProcessorThread.start()
|
|
|
|
# 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()
|
|
|
|
# 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 the cleanerThread
|
|
singleCleanerThread = singleCleaner()
|
|
# close the main program even if there are threads left
|
|
singleCleanerThread.daemon = True
|
|
singleCleanerThread.start()
|
|
|
|
# start network components if networking is enabled
|
|
if state.enableNetwork:
|
|
start_proxyconfig()
|
|
network.start(config, state)
|
|
|
|
if config.safeGetBoolean('bitmessagesettings', 'upnp'):
|
|
import upnp
|
|
upnpThread = upnp.uPnPThread()
|
|
upnpThread.start()
|
|
else:
|
|
network.connectionpool.pool.connectToStream(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:
|
|
try:
|
|
from pybitmessage.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
|