"Fossies" - the Fresh Open Source Software Archive

Member "Tardis-1.2.1/src/Tardis/Daemon.py" (9 Jun 2021, 23886 Bytes) of package /linux/privat/Tardis-1.2.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "Daemon.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.5_vs_1.2.1.

    1 # vim: set et sw=4 sts=4 fileencoding=utf-8:
    2 #
    3 # Tardis: A Backup System
    4 # Copyright 2013-2020, Eric Koldinger, All Rights Reserved.
    5 # kolding@washington.edu
    6 #
    7 # Redistribution and use in source and binary forms, with or without
    8 # modification, are permitted provided that the following conditions are met:
    9 #
   10 #     * Redistributions of source code must retain the above copyright
   11 #       notice, this list of conditions and the following disclaimer.
   12 #     * Redistributions in binary form must reproduce the above copyright
   13 #       notice, this list of conditions and the following disclaimer in the
   14 #       documentation and/or other materials provided with the distribution.
   15 #     * Neither the name of the copyright holder nor the
   16 #       names of its contributors may be used to endorse or promote products
   17 #       derived from this software without specific prior written permission.
   18 #
   19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   20 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   22 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   23 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   24 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   25 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   26 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   27 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   28 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   29 # POSSIBILITY OF SUCH DAMAGE.
   30 
   31 import os
   32 import sys
   33 import pwd
   34 import grp
   35 import argparse
   36 import uuid
   37 import logging
   38 import logging.config
   39 import configparser
   40 import socketserver
   41 import ssl
   42 import traceback
   43 import signal
   44 import threading
   45 import json
   46 import base64
   47 from datetime import datetime
   48 
   49 # For profiling
   50 import cProfile
   51 import io
   52 import pstats
   53 
   54 import daemonize
   55 import colorlog
   56 
   57 import Tardis
   58 import Tardis.Backend as Backend
   59 import Tardis.ConnIdLogAdapter as ConnIdLogAdapter
   60 import Tardis.Messages as Messages
   61 import Tardis.Util as Util
   62 import Tardis.Defaults as Defaults
   63 import Tardis.Connection as Connection
   64 
   65 DONE    = 0
   66 CONTENT = 1
   67 CKSUM   = 2
   68 DELTA   = 3
   69 REFRESH = 4                     # Perform a full content update
   70 LINKED  = 5                     # Check if it's already linked
   71 
   72 config = None
   73 args   = None
   74 configSection = 'Daemon'
   75 
   76 databaseName    = Defaults.getDefault('TARDIS_DBNAME')
   77 schemaName      = Defaults.getDefault('TARDIS_SCHEMA')
   78 configName      = Defaults.getDefault('TARDIS_DAEMON_CONFIG')
   79 baseDir         = Defaults.getDefault('TARDIS_DB')
   80 dbDir           = Defaults.getDefault('TARDIS_DBDIR')
   81 portNumber      = Defaults.getDefault('TARDIS_PORT')
   82 pidFileName     = Defaults.getDefault('TARDIS_PIDFILE')
   83 journalName     = Defaults.getDefault('TARDIS_JOURNAL')
   84 timeout         = Defaults.getDefault('TARDIS_TIMEOUT')
   85 logExceptions   = Defaults.getDefault('TARDIS_LOGEXCEPTIONS')
   86 skipFile        = Defaults.getDefault('TARDIS_SKIP')
   87 
   88 if  os.path.isabs(schemaName):
   89     schemaFile = schemaName
   90 else:
   91     parentDir    = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
   92     schemaFile   = os.path.join(parentDir, schemaName)
   93     # Hack.  Make it look shorter.
   94     schemaFile = min([schemaFile, os.path.relpath(schemaFile)], key=len)
   95     #if len(schemaFile) > len(os.path.relpath(schemaFile)):
   96         #schemaFile = os.path.relpath(schemaFile)
   97 
   98 configDefaults = {
   99     'Port'              : portNumber,
  100     'BaseDir'           : baseDir,
  101     'DBDir'             : dbDir,
  102     'DBName'            : databaseName,
  103     'Schema'            : schemaFile,
  104     'LogCfg'            : '',
  105     'Profile'           : str(False),
  106     'LogFile'           : '',
  107     'JournalFile'       : journalName,
  108     'LinkBasis'         : str(False),
  109     'LogExceptions'     : str(False),
  110     'AllowNewHosts'     : str(False),
  111     'RequirePassword'   : str(False),
  112     'Single'            : str(False),
  113     'Local'             : '',
  114     'Verbose'           : '0',
  115     'Daemon'            : str(False),
  116     'Umask'             : '027',
  117     'User'              : '',
  118     'Group'             : '',
  119     'SSL'               : str(False),
  120     'Timeout'           : timeout,
  121     'CertFile'          : '',
  122     'KeyFile'           : '',
  123     'PidFile'           : pidFileName,
  124     'ReuseAddr'         : str(False),
  125     'Formats'           : 'Monthly-%Y-%m, Weekly-%Y-%U, Daily-%Y-%m-%d',
  126     'Priorities'        : '40, 30, 20',
  127     'KeepDays'          : '0, 180, 30',
  128     'ForceFull'         : '0, 0, 0',
  129     'MaxDeltaChain'     : '5',
  130     'MaxChangePercent'  : '50',
  131     'SaveFull'          : str(False),
  132     'SkipFileName'      : skipFile,
  133     'DBBackups'         : '0',
  134     'CksContent'        : '65536',
  135     'AutoPurge'         : str(False),
  136     'SaveConfig'        : str(True),
  137     'AllowClientOverrides'  :  str(True),
  138     'AllowSchemaUpgrades'   :  str(False),
  139 }
  140 
  141 server = None
  142 logger = None
  143 
  144 logging.TRACE = logging.DEBUG - 1
  145 logging.MSGS  = logging.DEBUG - 2
  146 
  147 class InitFailedException(Exception):
  148     pass
  149 
  150 class ProtocolError(Exception):
  151     pass
  152 
  153 class TardisServerHandler(socketserver.BaseRequestHandler):
  154     numfiles = 0
  155     logger   = None
  156     sessionid = None
  157     tempdir = None
  158     cache   = None
  159     db      = None
  160     purged  = False
  161     full    = False
  162     statNewFiles = 0
  163     statUpdFiles = 0
  164     statDirs     = 0
  165     statBytesReceived = 0
  166     statPurgedFiles = 0
  167     statPurgedSets = 0
  168     statCommands = {}
  169     address = ''
  170     basedir = None
  171     autoPurge = False
  172     saveConfig = False
  173     forceFull = False
  174     saveFull = False
  175     lastCompleted = None
  176 
  177     def setup(self):
  178         if self.client_address:
  179             self.address = self.client_address[0]
  180         else:
  181             self.address = 'localhost'
  182         log            = logging.getLogger('Tardis')
  183         self.sessionid = str(uuid.uuid1())
  184         self.logger = ConnIdLogAdapter.ConnIdLogAdapter(log, {'connid': self.sessionid[0:13]})
  185         self.logger.info("Session created from: %s", self.address)
  186 
  187     def finish(self):
  188         self.logger.info("Ending session %s from %s", self.sessionid, self.address)
  189 
  190     def mkMessenger(self, sock, encoding, compress):
  191         """
  192         Create the messenger object to handle communications with the client
  193         """
  194         if encoding == "JSON":
  195             return Messages.JsonMessages(sock, compress=compress)
  196         elif encoding == 'MSGP':
  197             return Messages.MsgPackMessages(sock, compress=compress)
  198         elif encoding == "BSON":
  199             return Messages.BsonMessages(sock, compress=compress)
  200         else:
  201             message = {"status": "FAIL", "error": "Unknown encoding: {}".format(encoding)}
  202             sock.sendall(bytes(json.dumps(message), 'utf-8'))
  203             raise InitFailedException("Unknown encoding: ", encoding)
  204 
  205     def handle(self):
  206         started = False
  207         completed = False
  208         starttime = datetime.now()
  209 
  210         if self.server.profiler:
  211             self.logger.info("Starting Profiler")
  212             self.server.profiler.enable()
  213 
  214         try:
  215             sock = self.request
  216             sock.settimeout(args.timeout)
  217 
  218             if self.server.ssl:
  219                 sock.sendall(bytes(Connection.sslHeaderString, 'utf-8'))
  220                 sock = ssl.wrap_socket(sock, server_side=True, certfile=self.server.certfile, keyfile=self.server.keyfile)
  221             else:
  222                 sock.sendall(bytes(Connection.headerString, 'utf-8'))
  223 
  224             # Receive the initial messages.  Defines the communication parameters.
  225             # Should be : { "encoding": "MSGP", "compress": "snappy" }
  226 
  227             message = sock.recv(1024)
  228             self.logger.debug(message)
  229             message = str(message, 'utf-8').strip()
  230 
  231             fields = json.loads(message)
  232             resp = {'status': 'OK'}
  233             sock.sendall(bytes(json.dumps(resp), 'utf-8'))
  234 
  235             #self.addSession(self.sessionid, fields['host'])
  236 
  237             # Create the messenger object.  From this point on, ALL communications should
  238             # go through messenger, not director to the socket
  239             messenger = self.mkMessenger(sock, fields['encoding'], fields['compress'])
  240 
  241             # Create a backend, and run it.
  242             backend = Backend.Backend(messenger, self.server, sessionid=self.sessionid)
  243 
  244             (started, completed, endtime, orphansRemoved, orphanSize) = backend.runBackup()
  245 
  246             if self.server.profiler:
  247                 self.logger.info("Stopping Profiler")
  248                 self.server.profiler.disable()
  249                 s = io.StringIO()
  250                 sortby = 'cumulative'
  251                 ps = pstats.Stats(self.server.profiler, stream=s).sort_stats(sortby)
  252                 ps.print_stats()
  253                 print(s.getvalue())
  254 
  255         except InitFailedException as e:
  256             self.logger.error("Connection initialization failed: %s", e)
  257             if self.server.exceptions:
  258                 self.logger.exception(e)
  259         except Exception as e:
  260             self.logger.error("Caught exception %s: %s", type(e), e)
  261             if self.server.exceptions:
  262                 self.logger.exception(e)
  263         finally:
  264             if started:
  265                 self.logger.info("Connection completed successfully: %s  Runtime: %s", str(completed), str(endtime - starttime))
  266                 self.logger.info("New or replaced files:    %d", backend.statNewFiles)
  267                 self.logger.info("Updated files:            %d", backend.statUpdFiles)
  268                 self.logger.info("Total file data received: %s (%d)", Util.fmtSize(backend.statBytesReceived), backend.statBytesReceived)
  269                 self.logger.info("Command breakdown:        %s", backend.statCommands)
  270                 self.logger.info("Purged Sets and File:     %d %d", backend.statPurgedSets, backend.statPurgedFiles)
  271                 self.logger.info("Removed Orphans           %d (%s)", orphansRemoved, Util.fmtSize(orphanSize))
  272 
  273             self.logger.info("Session from %s {%s} Ending: %s: %s", backend.client, self.sessionid, str(completed), str(datetime.now() - starttime))
  274 
  275 class TardisServer(object):
  276     # HACK.  Operate on an object, but not in the class.
  277     # Want to do this in multiple classes.
  278     def __init__(self):
  279         self.basedir        = args.database
  280         if args.dbdir:
  281             self.dbdir      = args.dbdir
  282         else:
  283             self.dbdir      = self.basedir
  284         self.savefull       = config.getboolean(configSection, 'SaveFull')
  285         self.maxChain       = config.getint(configSection, 'MaxDeltaChain')
  286         self.deltaPercent   = float(config.getint(configSection, 'MaxChangePercent')) / 100.0        # Convert to a ratio
  287         self.cksContent     = config.getint(configSection, 'CksContent')
  288 
  289         self.dbname         = args.dbname
  290         self.allowNew       = args.newhosts
  291         self.schemaFile     = args.schema
  292         self.journal        = args.journal
  293 
  294         self.linkBasis      = config.getboolean(configSection, 'LinkBasis')
  295 
  296         self.requirePW      = config.getboolean(configSection, 'RequirePassword')
  297 
  298         self.allowOverrides = config.getboolean(configSection, 'AllowClientOverrides')
  299 
  300         self.allowUpgrades  = config.getboolean(configSection, 'AllowSchemaUpgrades')
  301 
  302         self.formats        = list(map(str.strip, config.get(configSection, 'Formats').split(',')))
  303         self.priorities     = list(map(int, config.get(configSection, 'Priorities').split(',')))
  304         self.keep           = list(map(int, config.get(configSection, 'KeepDays').split(',')))
  305         self.forceFull      = list(map(int, config.get(configSection, 'ForceFull').split(',')))
  306 
  307         self.timeout        = args.timeout
  308 
  309         numFormats = len(self.formats)
  310         if len(self.priorities) != numFormats or len(self.keep) != numFormats or len(self.forceFull) != numFormats:
  311             logger.warning("Different sizes for the lists of formats: Formats: %d Priorities: %d KeepDays: %d ForceFull: %d",
  312                            len(self.formats), len(self.priorities), len(self.keep), len(self.forceFull))
  313 
  314         self.dbbackups      = config.getint(configSection, 'DBBackups')
  315 
  316         self.exceptions     = args.exceptions
  317 
  318         self.umask          = Util.parseInt(config.get(configSection, 'Umask'))
  319 
  320         self.autoPurge      = config.getboolean(configSection, 'AutoPurge')
  321         self.saveConfig     = config.getboolean(configSection, 'SaveConfig')
  322 
  323         self.skip           = config.get(configSection, 'SkipFileName')
  324 
  325         self.user = None
  326         self.group = None
  327 
  328         self.sessions = {}
  329 
  330         # If the User or Group is set, attempt to determine the users
  331         # Note, these will throw exeptions if the User or Group is unknown.  Will get
  332         # passed up.
  333         if args.daemon:
  334             if args.user:
  335                 self.user = pwd.getpwnam(args.user).pw_uid
  336             if args.group:
  337                 self.group = grp.getgrnam(args.group).gr_gid
  338 
  339         # Get SSL set up, if it's been requested.
  340         self.ssl            = args.ssl
  341         self.certfile       = args.certfile
  342         self.keyfile        = args.keyfile
  343 
  344         # Create a session ID
  345         self.serverSessionID = str(uuid.uuid1())
  346 
  347         if args.profile:
  348             self.profiler = cProfile.Profile()
  349         else:
  350             self.profiler = None
  351 
  352 #class TardisSocketServer(SocketServer.TCPServer):
  353 class TardisSocketServer(socketserver.ThreadingMixIn, socketserver.TCPServer, TardisServer):
  354     def __init__(self):
  355 
  356         socketserver.TCPServer.__init__(self, ("", args.port), TardisServerHandler)
  357         TardisServer.__init__(self)
  358         logger.info("TCP Server %s Running", Tardis.__versionstring__)
  359 
  360 class TardisSingleThreadedSocketServer(socketserver.TCPServer, TardisServer):
  361     def __init__(self):
  362         socketserver.TCPServer.__init__(self, ("", args.port), TardisServerHandler)
  363         TardisServer.__init__(self)
  364         logger.info("Single Threaded TCP Server %s Running", Tardis.__versionstring__)
  365 
  366 class TardisDomainSocketServer(socketserver.UnixStreamServer, TardisServer):
  367     def __init__(self):
  368         socketserver.UnixStreamServer.__init__(self,  args.local, TardisServerHandler)
  369         TardisServer.__init__(self)
  370         logger.info("Unix Domain Socket %s Server Running", Tardis.__versionstring__)
  371 
  372 
  373 def setupLogging():
  374     levels = [logging.WARNING, logging.INFO, logging.DEBUG, logging.TRACE]
  375 
  376     logging.addLevelName(logging.TRACE, 'Message')
  377     logging.addLevelName(logging.MSGS,  'MSG')
  378 
  379     logging.raiseExceptions = False
  380 
  381     if args.logcfg:
  382         logging.config.fileConfig(args.logcfg)
  383         logger = logging.getLogger('')
  384     else:
  385         logger = logging.getLogger('')
  386         if args.logfile or args.daemon:
  387             logFormat = logging.Formatter("%(asctime)s %(levelname)s : %(message)s")
  388         else:
  389             # Create some default colors
  390             colors = {
  391                 'DEBUG':    'cyan',
  392                 'INFO':     'green',
  393                 'WARNING':  'yellow',
  394                 'ERROR':    'red',
  395                 'CRITICAL': 'red,bg_white',
  396             }
  397             logFormat = colorlog.TTYColoredFormatter("%(asctime)s %(log_color)s%(levelname)s%(reset)s : %(message)s", log_colors=colors, stream=sys.stdout)
  398 
  399         verbosity = args.verbose
  400 
  401         if args.local:
  402             # Always send output to stderr for local connections
  403             handler = logging.StreamHandler()
  404         elif args.logfile:
  405             handler = logging.handlers.WatchedFileHandler(args.logfile)
  406         elif args.daemon:
  407             handler = logging.handlers.SysLogHandler()
  408         else:
  409             handler = logging.StreamHandler()
  410 
  411         handler.setFormatter(logFormat)
  412         logger.addHandler(handler)
  413 
  414         loglevel = levels[verbosity] if verbosity < len(levels) else levels[-1]
  415         logger.setLevel(loglevel)
  416 
  417     return logger
  418 
  419 def runServer():
  420     global server
  421 
  422     try:
  423         if args.reuseaddr:
  424             # Allow reuse of the address before timeout if requested.
  425             socketserver.TCPServer.allow_reuse_address = True
  426 
  427         if args.local:
  428             logger.info("Starting Server. Socket: %s", args.local)
  429             server = TardisDomainSocketServer()
  430         elif args.threaded:
  431             logger.info("Starting Server on Port: %d", config.getint(configSection, 'Port'))
  432             server = TardisSocketServer()
  433         else:
  434             logger.info("Starting Single Threaded Server on Port: %d", config.getint(configSection, 'Port'))
  435             server = TardisSingleThreadedSocketServer()
  436 
  437         logger.info("Server Session: %s", server.serverSessionID)
  438 
  439         if args.single:
  440             server.handle_request()
  441         else:
  442             try:
  443                 server.serve_forever()
  444             except:
  445                 logger.info("Socket server completed")
  446         logger.info("Ending")
  447     except Exception as e:
  448         logger.critical("Unable to run server: {}".format(e))
  449         if args.exceptions:
  450             logger.exception(e)
  451 
  452 def stopServer():
  453     logger.info("Stopping server")
  454     server.shutdown()
  455 
  456 def signalTermHandler(signal, frame):
  457     logger.info("Caught term signal.  Stopping")
  458     t = threading.Thread(target = shutdownHandler)
  459     t.start()
  460     logger.info("Server stopped")
  461 
  462 def shutdownHandler():
  463     stopServer()
  464 
  465 def processArgs():
  466     parser = argparse.ArgumentParser(description='Tardis Backup Server', formatter_class=Util.HelpFormatter, add_help=False)
  467 
  468     parser.add_argument('--config',         dest='config', default=configName, help="Location of the configuration file (Default: %(default)s)")
  469     (args, remaining) = parser.parse_known_args()
  470 
  471     t = configSection
  472     config = configparser.RawConfigParser(configDefaults, default_section='Tardis')
  473     config.add_section(t)                   # Make it safe for reading other values from.
  474     if args.config:
  475         config.read(args.config)
  476 
  477     parser.add_argument('--port',               dest='port',            default=config.getint(t, 'Port'), type=int, help='Listen on port (Default: %(default)s)')
  478     parser.add_argument('--database',           dest='database',        default=config.get(t, 'BaseDir'), help='Dabatase directory (Default: %(default)s)')
  479     parser.add_argument('--dbdir',              dest='dbdir',           default=config.get(t, 'DBDir'),  help='Dabatase directory (Default: %(default)s)')
  480     parser.add_argument('--dbname',             dest='dbname',          default=config.get(t, 'DBName'), help='Use the database name (Default: %(default)s)')
  481     parser.add_argument('--schema',             dest='schema',          default=config.get(t, 'Schema'), help='Path to the schema to use (Default: %(default)s)')
  482     parser.add_argument('--logfile', '-l',      dest='logfile',         default=config.get(t, 'LogFile'), help='Log to file (Default: %(default)s)')
  483     parser.add_argument('--logcfg',             dest='logcfg',          default=config.get(t, 'LogCfg'), help='Logging configuration file')
  484     parser.add_argument('--verbose', '-v',      dest='verbose',         action='count', default=config.getint(t, 'Verbose'), help='Increase the verbosity (may be repeated)')
  485     parser.add_argument('--exceptions',         dest='exceptions',      action=Util.StoreBoolean, default=config.getboolean(t, 'LogExceptions'), help='Log full exception details')
  486     parser.add_argument('--allow-new-hosts',    dest='newhosts',        action=Util.StoreBoolean, default=config.getboolean(t, 'AllowNewHosts'),
  487                         help='Allow new clients to attach and create new backup sets')
  488     parser.add_argument('--profile',            dest='profile',         default=config.getboolean(t, 'Profile'), help='Generate a profile')
  489 
  490     parser.add_argument('--single',             dest='single',          action=Util.StoreBoolean, default=config.getboolean(t, 'Single'),
  491                         help='Run a single transaction and quit')
  492     parser.add_argument('--local',              dest='local',           default=config.get(t, 'Local'),
  493                         help='Run as a Unix Domain Socket Server on the specified filename')
  494     parser.add_argument('--threads',            dest='threaded',        action=Util.StoreBoolean, default=True, help='Run a threaded server.  Default: %(default)s')
  495 
  496     parser.add_argument('--timeout',            dest='timeout',         default=config.getint(t, 'Timeout'), type=float, help='Timeout, in seconds.  0 for no timeout (Default: %(default)s)')
  497     parser.add_argument('--journal', '-j',      dest='journal',         default=config.get(t, 'JournalFile'), help='Journal file actions to this file (Default: %(default)s)')
  498 
  499     parser.add_argument('--reuseaddr',          dest='reuseaddr',       action=Util.StoreBoolean, default=config.getboolean(t, 'ReuseAddr'),
  500                         help='Reuse the socket address immediately')
  501 
  502     parser.add_argument('--daemon',             dest='daemon',          action=Util.StoreBoolean, default=config.getboolean(t, 'Daemon'),
  503                         help='Run as a daemon')
  504     parser.add_argument('--user',               dest='user',            default=config.get(t, 'User'), help='Run daemon as user.  Valid only if --daemon is set')
  505     parser.add_argument('--group',              dest='group',           default=config.get(t, 'Group'), help='Run daemon as group.  Valid only if --daemon is set')
  506     parser.add_argument('--pidfile',            dest='pidfile',         default=config.get(t, 'PidFile'), help='Use this pidfile to indicate running daemon')
  507 
  508     parser.add_argument('--ssl',                dest='ssl',             action=Util.StoreBoolean, default=config.getboolean(t, 'SSL'), help='Use SSL connections')
  509     parser.add_argument('--certfile',           dest='certfile',        default=config.get(t, 'CertFile'), help='Path to certificate file for SSL connections')
  510     parser.add_argument('--keyfile',            dest='keyfile',         default=config.get(t, 'KeyFile'), help='Path to key file for SSL connections')
  511 
  512     parser.add_argument('--version',            action='version', version='%(prog)s ' + Tardis.__versionstring__,    help='Show the version')
  513     parser.add_argument('--help', '-h',         action='help')
  514 
  515     Util.addGenCompletions(parser)
  516 
  517     args = parser.parse_args(remaining)
  518     return(args, config)
  519 
  520 def main():
  521     global logger, args, config
  522     (args, config) = processArgs()
  523 
  524     # Set up a handler
  525     signal.signal(signal.SIGTERM, signalTermHandler)
  526     try:
  527         logger = setupLogging()
  528     except Exception as e:
  529         print("Unable to initialize logging: {}".format(str(e)), file=sys.stderr)
  530         if args.exceptions:
  531             traceback.print_exc()
  532         sys.exit(1)
  533 
  534     if args.daemon and not args.local:
  535         user  = args.user
  536         group = args.group
  537         pidfile = args.pidfile
  538         fds = [h.stream.fileno() for h in logger.handlers if isinstance(h, logging.StreamHandler)]
  539         logger.info("About to daemonize")
  540 
  541         try:
  542             daemon = daemonize.Daemonize(app="tardisd", pid=pidfile, action=runServer, user=user, group=group, keep_fds=fds)
  543             daemon.start()
  544         except Exception as e:
  545             logger.critical("Caught Exception on Daemonize call: {}".format(e))
  546             if args.exceptions:
  547                 logger.exception(e)
  548     else:
  549         try:
  550             runServer()
  551         except KeyboardInterrupt:
  552             logger.warning("Killed by Keyboard")
  553             pass
  554         except Exception as e:
  555             logger.critical("Unable to run server: {}".format(e))
  556             if args.exceptions:
  557                 logger.exception(e)
  558 
  559 if __name__ == "__main__":
  560     try:
  561         sys.exit(main())
  562     except Exception as e:
  563         traceback.print_exc()