/ src / bitmessagemain.py
bitmessagemain.py
  1  #!/usr/bin/env python
  2  """
  3  The PyBitmessage startup script
  4  """
  5  # Copyright (c) 2012-2016 Jonathan Warren
  6  # Copyright (c) 2012-2022 The Bitmessage developers
  7  # Distributed under the MIT/X11 software license. See the accompanying
  8  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
  9  
 10  # Right now, PyBitmessage only support connecting to stream 1. It doesn't
 11  # yet contain logic to expand into further streams.
 12  import os
 13  import sys
 14  
 15  
 16  try:
 17      from . import pathmagic
 18  except ImportError:
 19      from pybitmessage import pathmagic
 20  app_dir = pathmagic.setup()
 21  
 22  from . import depends
 23  depends.check_dependencies()
 24  
 25  import getopt
 26  import multiprocessing
 27  # Used to capture a Ctrl-C keypress so that Bitmessage can shutdown gracefully.
 28  import signal
 29  import threading
 30  import time
 31  import traceback
 32  
 33  from . import defaults
 34  # Network subsystem
 35  from . import network
 36  from . import shutdown
 37  from . import state
 38  
 39  from .testmode_init import populate_api_test_data
 40  from .bmconfigparser import config
 41  from .debug import logger  # this should go before any threads
 42  from .helper_startup import (
 43      adjustHalfOpenConnectionsLimit, fixSocket, start_proxyconfig)
 44  from .inventory import Inventory
 45  from .singleinstance import singleinstance
 46  # Synchronous threads
 47  from .threads import (
 48      set_thread_name, printLock,
 49      addressGenerator, objectProcessor, singleCleaner, singleWorker, sqlThread)
 50  
 51  
 52  def signal_handler(signum, frame):
 53      """Single handler for any signal sent to pybitmessage"""
 54      process = multiprocessing.current_process()
 55      thread = threading.current_thread()
 56      logger.error(
 57          'Got signal %i in %s/%s',
 58          signum, process.name, thread.name
 59      )
 60      if process.name == "RegExParser":
 61          # on Windows this isn't triggered, but it's fine,
 62          # it has its own process termination thing
 63          raise SystemExit
 64      if "PoolWorker" in process.name:
 65          raise SystemExit
 66      if thread.name not in ("PyBitmessage", "MainThread"):
 67          return
 68      logger.error("Got signal %i", signum)
 69      # there are possible non-UI variants to run bitmessage
 70      # which should shutdown especially test-mode
 71      if state.thisapp.daemon or not state.enableGUI:
 72          shutdown.doCleanShutdown()
 73      else:
 74          print(('# Thread: %s(%d)' % (thread.name, thread.ident)))
 75          for filename, lineno, name, line in traceback.extract_stack(frame):
 76              print(('File: "%s", line %d, in %s' % (filename, lineno, name)))
 77              if line:
 78                  print(('  %s' % line.strip()))
 79          print('Unfortunately you cannot use Ctrl+C when running the UI'
 80                ' because the UI captures the signal.')
 81  
 82  
 83  class Main(object):
 84      """Main PyBitmessage class"""
 85      def start(self):
 86          """Start main application"""
 87          # pylint: disable=too-many-statements,too-many-branches,too-many-locals
 88          fixSocket()
 89          adjustHalfOpenConnectionsLimit()
 90  
 91          daemon = config.safeGetBoolean('bitmessagesettings', 'daemon')
 92  
 93          try:
 94              opts, _ = getopt.getopt(
 95                  sys.argv[1:], "hcdt",
 96                  ["help", "curses", "daemon", "test"])
 97  
 98          except getopt.GetoptError:
 99              self.usage()
100              sys.exit(2)
101  
102          for opt, _ in opts:
103              if opt in ("-h", "--help"):
104                  self.usage()
105                  sys.exit()
106              elif opt in ("-d", "--daemon"):
107                  daemon = True
108              elif opt in ("-c", "--curses"):
109                  state.curses = True
110              elif opt in ("-t", "--test"):
111                  state.testmode = True
112                  if os.path.isfile(os.path.join(
113                          state.appdata, 'unittest.lock')):
114                      daemon = True
115                  state.enableGUI = False  # run without a UI
116                  # Fallback: in case when no api command was issued
117                  state.last_api_response = time.time()
118                  # Apply special settings
119                  config.set(
120                      'bitmessagesettings', 'apienabled', 'true')
121                  config.set(
122                      'bitmessagesettings', 'apiusername', 'username')
123                  config.set(
124                      'bitmessagesettings', 'apipassword', 'password')
125                  config.set(
126                      'bitmessagesettings', 'apivariant', 'legacy')
127                  config.set(
128                      'bitmessagesettings', 'apinotifypath',
129                      os.path.join(app_dir, 'tests', 'apinotify_handler.py')
130                  )
131  
132          if daemon:
133              state.enableGUI = False  # run without a UI
134  
135          if state.enableGUI and not state.curses and not depends.check_pyqt():
136              sys.exit(
137                  'PyBitmessage requires PyQt unless you want'
138                  ' to run it as a daemon and interact with it'
139                  ' using the API. You can download PyQt from '
140                  'http://www.riverbankcomputing.com/software/pyqt/download'
141                  ' or by searching Google for \'PyQt Download\'.'
142                  ' If you want to run in daemon mode, see '
143                  'https://bitmessage.org/wiki/Daemon\n'
144                  'You can also run PyBitmessage with'
145                  ' the new curses interface by providing'
146                  ' \'-c\' as a commandline argument.'
147              )
148          # is the application already running?  If yes then exit.
149          state.thisapp = singleinstance("", daemon)
150  
151          if daemon:
152              with printLock:
153                  print('Running as a daemon. Send TERM signal to end.')
154              self.daemonize()
155  
156          self.setSignalHandler()
157  
158          set_thread_name("PyBitmessage")
159  
160          if state.testmode or config.safeGetBoolean(
161                  'bitmessagesettings', 'extralowdifficulty'):
162              defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int(
163                  defaults.networkDefaultProofOfWorkNonceTrialsPerByte / 100)
164              defaults.networkDefaultPayloadLengthExtraBytes = int(
165                  defaults.networkDefaultPayloadLengthExtraBytes / 100)
166  
167          # Start the SQL thread
168          sqlLookup = sqlThread()
169          # DON'T close the main program even if there are threads left.
170          # The closeEvent should command this thread to exit gracefully.
171          sqlLookup.daemon = False
172          sqlLookup.start()
173          state.Inventory = Inventory()  # init
174  
175          if state.enableObjProc:  # Not needed if objproc is disabled
176              # Start the address generation thread
177              addressGeneratorThread = addressGenerator()
178              # close the main program even if there are threads left
179              addressGeneratorThread.daemon = True
180              addressGeneratorThread.start()
181  
182              # Start the thread that calculates POWs
183              singleWorkerThread = singleWorker()
184              # close the main program even if there are threads left
185              singleWorkerThread.daemon = True
186              singleWorkerThread.start()
187  
188              # Start the object processing thread
189              objectProcessorThread = objectProcessor()
190              # DON'T close the main program even if the thread remains.
191              # This thread checks the shutdown variable after processing
192              # each object.
193              objectProcessorThread.daemon = False
194              objectProcessorThread.start()
195  
196              # SMTP delivery thread
197              if daemon and config.safeGet(
198                      'bitmessagesettings', 'smtpdeliver', '') != '':
199                  from .class_smtpDeliver import smtpDeliver
200                  smtpDeliveryThread = smtpDeliver()
201                  smtpDeliveryThread.start()
202  
203              # SMTP daemon thread
204              if daemon and config.safeGetBoolean(
205                      'bitmessagesettings', 'smtpd'):
206                  from .class_smtpServer import smtpServer
207                  smtpServerThread = smtpServer()
208                  smtpServerThread.start()
209  
210              # API is also objproc dependent
211              if config.safeGetBoolean('bitmessagesettings', 'apienabled'):
212                  from . import api  # pylint: disable=relative-import
213                  singleAPIThread = api.singleAPI()
214                  # close the main program even if there are threads left
215                  singleAPIThread.daemon = True
216                  singleAPIThread.start()
217  
218          # Start the cleanerThread
219          singleCleanerThread = singleCleaner()
220          # close the main program even if there are threads left
221          singleCleanerThread.daemon = True
222          singleCleanerThread.start()
223  
224          # start network components if networking is enabled
225          if state.enableNetwork:
226              start_proxyconfig()
227              network.start(config, state)
228  
229              if config.safeGetBoolean('bitmessagesettings', 'upnp'):
230                  from . import upnp
231                  upnpThread = upnp.uPnPThread()
232                  upnpThread.start()
233          else:
234              network.connectionpool.pool.connectToStream(1)
235  
236          if not daemon and state.enableGUI:
237              if state.curses:
238                  if not depends.check_curses():
239                      sys.exit()
240                  print('Running with curses')
241                  from . import bitmessagecurses
242                  bitmessagecurses.runwrapper()
243              else:
244                  from . import bitmessageqt
245                  bitmessageqt.run()
246          else:
247              config.remove_option('bitmessagesettings', 'dontconnect')
248  
249          if state.testmode:
250              populate_api_test_data()
251  
252          if daemon:
253              while state.shutdown == 0:
254                  time.sleep(1)
255                  if (
256                      state.testmode
257                      and time.time() - state.last_api_response >= 30
258                  ):
259                      self.stop()
260          elif not state.enableGUI:
261              state.enableGUI = True
262              try:
263                  # pylint: disable=relative-import
264                  from .tests import core as test_core
265              except ImportError:
266                  try:
267                      from pybitmessage.tests import core as test_core
268                  except ImportError:
269                      self.stop()
270                      return
271  
272              test_core_result = test_core.run()
273              self.stop()
274              test_core.cleanup()
275              sys.exit(not test_core_result.wasSuccessful())
276  
277      @staticmethod
278      def daemonize():
279          """Running as a daemon. Send signal in end."""
280          grandfatherPid = os.getpid()
281          parentPid = None
282          try:
283              if os.fork():
284                  # unlock
285                  state.thisapp.cleanup()
286                  # wait until grandchild ready
287                  while True:
288                      time.sleep(1)
289                  os._exit(0)  # pylint: disable=protected-access
290          except AttributeError:
291              # fork not implemented
292              pass
293          else:
294              parentPid = os.getpid()
295              state.thisapp.lock()  # relock
296  
297          os.umask(0)
298          try:
299              os.setsid()
300          except AttributeError:
301              # setsid not implemented
302              pass
303          try:
304              if os.fork():
305                  # unlock
306                  state.thisapp.cleanup()
307                  # wait until child ready
308                  while True:
309                      time.sleep(1)
310                  os._exit(0)  # pylint: disable=protected-access
311          except AttributeError:
312              # fork not implemented
313              pass
314          else:
315              state.thisapp.lock()  # relock
316          state.thisapp.lockPid = None  # indicate we're the final child
317          sys.stdout.flush()
318          sys.stderr.flush()
319          if not sys.platform.startswith('win'):
320              si = open(os.devnull, 'r')
321              so = open(os.devnull, 'a+')
322              se = open(os.devnull, 'a+', 0)
323              os.dup2(si.fileno(), sys.stdin.fileno())
324              os.dup2(so.fileno(), sys.stdout.fileno())
325              os.dup2(se.fileno(), sys.stderr.fileno())
326          if parentPid:
327              # signal ready
328              os.kill(parentPid, signal.SIGTERM)
329              os.kill(grandfatherPid, signal.SIGTERM)
330  
331      @staticmethod
332      def setSignalHandler():
333          """Setting the Signal Handler"""
334          signal.signal(signal.SIGINT, signal_handler)
335          signal.signal(signal.SIGTERM, signal_handler)
336          # signal.signal(signal.SIGINT, signal.SIG_DFL)
337  
338      @staticmethod
339      def usage():
340          """Displaying the usages"""
341          print(('Usage: ' + sys.argv[0] + ' [OPTIONS]'))
342          print('''
343  Options:
344    -h, --help            show this help message and exit
345    -c, --curses          use curses (text mode) interface
346    -d, --daemon          run in daemon (background) mode
347    -t, --test            dryrun, make testing
348  
349  All parameters are optional.
350  ''')
351  
352      @staticmethod
353      def stop():
354          """Stop main application"""
355          with printLock:
356              print('Stopping Bitmessage Deamon.')
357          shutdown.doCleanShutdown()
358  
359      # .. todo:: nice function but no one is using this
360      @staticmethod
361      def getApiAddress():
362          """This function returns API address and port"""
363          if not config.safeGetBoolean(
364                  'bitmessagesettings', 'apienabled'):
365              return None
366          address = config.get('bitmessagesettings', 'apiinterface')
367          port = config.getint('bitmessagesettings', 'apiport')
368          return {'address': address, 'port': port}
369  
370  
371  def main():
372      """Triggers main module"""
373      mainprogram = Main()
374      mainprogram.start()
375  
376  
377  if __name__ == "__main__":
378      main()
379  
380  
381  # So far, the creation of and management of the Bitmessage protocol and this
382  # client is a one-man operation. Bitcoin tips are quite appreciated.
383  # 1H5XaDA6fYENLbknwZyjiYXYPQaFjjLX2u