]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/scripts/startstop_node.py
wrap long lines, and tolerate various-width wrappings of the --help output
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / startstop_node.py
index aa7bc0c3e084770e9b844b9249a77875c05820c1..e3b2e72c4489e02636a0a45625c9a703fdaafa7b 100644 (file)
 
 import os, sys, signal, time
+from allmydata.scripts.common import BasedirOptions
+from twisted.scripts import twistd
 from twisted.python import usage
-from allmydata.scripts.common import BasedirMixin
-from allmydata.util import fileutil, find_exe
+from allmydata.scripts.default_nodedir import _default_nodedir
+from allmydata.util import fileutil
+from allmydata.util.encodingutil import listdir_unicode, quote_local_unicode_path
 
-class StartOptions(BasedirMixin, usage.Options):
-    optParameters = [
-        ["basedir", "C", None, "which directory to start the node in"],
-        ]
-    optFlags = [
-        ["profile", "p", "whether to run under the Python profiler, putting results in \"profiling_results.prof\""],
-        ]
 
-class StopOptions(BasedirMixin, usage.Options):
+class StartOptions(BasedirOptions):
+    subcommand_name = "start"
     optParameters = [
-        ["basedir", "C", None, "which directory to stop the node in"],
+        ("basedir", "C", None,
+         "Specify which Tahoe base directory should be used."
+         " This has the same effect as the global --node-directory option."
+         " [default: %s]" % quote_local_unicode_path(_default_nodedir)),
         ]
 
-class RestartOptions(BasedirMixin, usage.Options):
-    optParameters = [
-        ["basedir", "C", None, "which directory to restart the node in"],
-        ]
-    optFlags = [
-        ["force", "f", "if the node is not already running, start it "
-         "instead of complaining that you should have used 'start' instead "
-         "of 'restart'"],
-        ["profile", "p", "whether to run under the Python profiler, putting results in \"profiling_results.prof\""],
-        ]
+    def parseArgs(self, basedir=None, *twistd_args):
+        # This can't handle e.g. 'tahoe start --nodaemon', since '--nodaemon'
+        # looks like an option to the tahoe subcommand, not to twistd. So you
+        # can either use 'tahoe start' or 'tahoe start NODEDIR
+        # --TWISTD-OPTIONS'. Note that 'tahoe --node-directory=NODEDIR start
+        # --TWISTD-OPTIONS' also isn't allowed, unfortunately.
 
-class RunOptions(usage.Options):
-    optParameters = [
-        ["basedir", "C", None, "which directory to run the node in, CWD by default"],
-        ]
+        BasedirOptions.parseArgs(self, basedir)
+        self.twistd_args = twistd_args
+
+    def getSynopsis(self):
+        return ("Usage:  %s [global-options] %s [options]"
+                " [NODEDIR [twistd-options]]"
+                % (self.command_name, self.subcommand_name))
+
+    def getUsage(self, width=None):
+        t = BasedirOptions.getUsage(self, width) + "\n"
+        twistd_options = str(MyTwistdConfig()).partition("\n")[2].partition("\n\n")[0]
+        t += twistd_options.replace("Options:", "twistd-options:", 1)
+        t += """
 
-def do_start(basedir, profile=False, out=sys.stdout, err=sys.stderr):
-    print >>out, "STARTING", basedir
-    if os.path.exists(os.path.join(basedir, "client.tac")):
-        tac = "client.tac"
-        type = "client"
-    elif os.path.exists(os.path.join(basedir, "tahoe-client.tac")):
-        tac = "tahoe-client.tac"
-        type = "client"
-    elif os.path.exists(os.path.join(basedir, "introducer.tac")):
-        tac = "introducer.tac"
-        type = "introducer"
-    elif os.path.exists(os.path.join(basedir, "tahoe-introducer.tac")):
-        tac = "tahoe-introducer.tac"
-        type = "introducer"
+Note that if any twistd-options are used, NODEDIR must be specified explicitly
+(not by default or using -C/--basedir or -d/--node-directory), and followed by
+the twistd-options.
+"""
+        return t
+
+class StopOptions(BasedirOptions):
+    def parseArgs(self, basedir=None):
+        BasedirOptions.parseArgs(self, basedir)
+
+    def getSynopsis(self):
+        return ("Usage:  %s [global-options] stop [options] [NODEDIR]"
+                % (self.command_name,))
+
+class RestartOptions(StartOptions):
+    subcommand_name = "restart"
+
+class RunOptions(StartOptions):
+    subcommand_name = "run"
+
+
+class MyTwistdConfig(twistd.ServerOptions):
+    subCommands = [("StartTahoeNode", None, usage.Options, "node")]
+
+class StartTahoeNodePlugin:
+    tapname = "tahoenode"
+    def __init__(self, nodetype, basedir):
+        self.nodetype = nodetype
+        self.basedir = basedir
+    def makeService(self, so):
+        # delay this import as late as possible, to allow twistd's code to
+        # accept --reactor= selection. N.B.: this can't actually work until
+        # this file, and all the __init__.py files above it, also respect the
+        # prohibition on importing anything that transitively imports
+        # twisted.internet.reactor . That will take a lot of work.
+        if self.nodetype == "client":
+            from allmydata.client import Client
+            return Client(self.basedir)
+        if self.nodetype == "introducer":
+            from allmydata.introducer.server import IntroducerNode
+            return IntroducerNode(self.basedir)
+        if self.nodetype == "key-generator":
+            from allmydata.key_generator import KeyGeneratorService
+            return KeyGeneratorService(default_key_size=2048)
+        if self.nodetype == "stats-gatherer":
+            from allmydata.stats import StatsGathererService
+            return StatsGathererService(verbose=True)
+        raise ValueError("unknown nodetype %s" % self.nodetype)
+
+def identify_node_type(basedir):
+    for fn in listdir_unicode(basedir):
+        if fn.endswith(u".tac"):
+            tac = str(fn)
+            break
     else:
-        print >>err, "%s does not look like a node directory" % basedir
-        if not os.path.isdir(basedir):
-            print >>err, " in fact, it doesn't look like a directory at all!"
-        return 1
+        return None
 
-    cmd = find_exe.find_exe('twistd')
-    if not cmd:
-        print "Can't find twistd (it comes with Twisted).  Aborting."
-        sys.exit(1)
+    for t in ("client", "introducer", "key-generator", "stats-gatherer"):
+        if t in tac:
+            return t
+    return None
 
-    fileutil.make_dirs(os.path.join(basedir, "logs"))
-    cmd.extend(["-y", tac, "--logfile", os.path.join("logs", "twistd.log")])
-    if profile:
-        cmd.extend(["--profile=profiling_results.prof", "--savestats",])
-    curdir = os.getcwd()
+def start(config, out=sys.stdout, err=sys.stderr):
+    basedir = config['basedir']
+    quoted_basedir = quote_local_unicode_path(basedir)
+    print >>out, "STARTING", quoted_basedir
+    if not os.path.isdir(basedir):
+        print >>err, "%s does not look like a directory at all" % quoted_basedir
+        return 1
+    nodetype = identify_node_type(basedir)
+    if not nodetype:
+        print >>err, "%s is not a recognizable node directory" % quoted_basedir
+        return 1
+    # Now prepare to turn into a twistd process. This os.chdir is the point
+    # of no return.
+    os.chdir(basedir)
+    twistd_args = []
+    if (nodetype in ("client", "introducer")
+        and "--nodaemon" not in config.twistd_args
+        and "--syslog" not in config.twistd_args
+        and "--logfile" not in config.twistd_args):
+        fileutil.make_dirs(os.path.join(basedir, u"logs"))
+        twistd_args.extend(["--logfile", os.path.join("logs", "twistd.log")])
+    twistd_args.extend(config.twistd_args)
+    twistd_args.append("StartTahoeNode") # point at our StartTahoeNodePlugin
+
+    twistd_config = MyTwistdConfig()
     try:
-        os.chdir(basedir)
-        rc = os.system(' '.join(cmd))
-    finally:
-        os.chdir(curdir)
-    if rc == 0:
-        print >>out, "%s node probably started" % type
-        return 0
-    else:
-        print >>err, "%s node probably not started" % type
+        twistd_config.parseOptions(twistd_args)
+    except usage.error, ue:
+        # these arguments were unsuitable for 'twistd'
+        print >>err, config
+        print >>err, "tahoe %s: usage error from twistd: %s\n" % (config.subcommand_name, ue)
         return 1
+    twistd_config.loadedPlugins = {"StartTahoeNode": StartTahoeNodePlugin(nodetype, basedir)}
+
+    # On Unix-like platforms:
+    #   Unless --nodaemon was provided, the twistd.runApp() below spawns off a
+    #   child process, and the parent calls os._exit(0), so there's no way for
+    #   us to get control afterwards, even with 'except SystemExit'. If
+    #   application setup fails (e.g. ImportError), runApp() will raise an
+    #   exception.
+    #
+    #   So if we wanted to do anything with the running child, we'd have two
+    #   options:
+    #
+    #    * fork first, and have our child wait for the runApp() child to get
+    #      running. (note: just fork(). This is easier than fork+exec, since we
+    #      don't have to get PATH and PYTHONPATH set up, since we're not
+    #      starting a *different* process, just cloning a new instance of the
+    #      current process)
+    #    * or have the user run a separate command some time after this one
+    #      exits.
+    #
+    #   For Tahoe, we don't need to do anything with the child, so we can just
+    #   let it exit.
+    #
+    # On Windows:
+    #   twistd does not fork; it just runs in the current process whether or not
+    #   --nodaemon is specified. (As on Unix, --nodaemon does have the side effect
+    #   of causing us to log to stdout/stderr.)
+
+    if "--nodaemon" in twistd_args or sys.platform == "win32":
+        verb = "running"
+    else:
+        verb = "starting"
+
+    print >>out, "%s node in %s" % (verb, quoted_basedir)
+    twistd.runApp(twistd_config)
+    # we should only reach here if --nodaemon or equivalent was used
+    return 0
 
-def do_stop(basedir, out=sys.stdout, err=sys.stderr):
-    print >>out, "STOPPING", basedir
-    pidfile = os.path.join(basedir, "twistd.pid")
+def stop(config, out=sys.stdout, err=sys.stderr):
+    basedir = config['basedir']
+    quoted_basedir = quote_local_unicode_path(basedir)
+    print >>out, "STOPPING", quoted_basedir
+    pidfile = os.path.join(basedir, u"twistd.pid")
     if not os.path.exists(pidfile):
-        print >>err, "%s does not look like a running node directory (no twistd.pid)" % basedir
+        print >>err, "%s does not look like a running node directory (no twistd.pid)" % quoted_basedir
+        # we define rc=2 to mean "nothing is running, but it wasn't me who
+        # stopped it"
         return 2
     pid = open(pidfile, "r").read()
     pid = int(pid)
 
     # kill it hard (SIGKILL), delete the twistd.pid file, then wait for the
-    # process itself to go away. If it hasn't gone away after 5 seconds, warn
+    # process itself to go away. If it hasn't gone away after 20 seconds, warn
     # the user but keep waiting until they give up.
     try:
         os.kill(pid, signal.SIGKILL)
@@ -94,7 +193,7 @@ def do_stop(basedir, out=sys.stdout, err=sys.stderr):
             print oserr.strerror
             # the process didn't exist, so wipe the pid file
             os.remove(pidfile)
-            return 1
+            return 2
         else:
             raise
     try:
@@ -103,7 +202,7 @@ def do_stop(basedir, out=sys.stdout, err=sys.stderr):
         pass
     start = time.time()
     time.sleep(0.1)
-    wait = 5
+    wait = 40
     first_time = True
     while True:
         # poll once per second until we see the process is no longer running
@@ -126,58 +225,26 @@ def do_stop(basedir, out=sys.stdout, err=sys.stderr):
                       (pid, (time.time() - start))
                 wait = 10
         time.sleep(1)
+    # we define rc=1 to mean "I think something is still running, sorry"
     return 1
 
-def start(config, stdout, stderr):
-    rc = 0
-    for basedir in config['basedirs']:
-        rc = do_start(basedir, config['profile'], stdout, stderr) or rc
-    return rc
-
-def stop(config, stdout, stderr):
-    rc = 0
-    for basedir in config['basedirs']:
-        rc = do_stop(basedir, stdout, stderr) or rc
-    return rc
-
 def restart(config, stdout, stderr):
-    rc = 0
-    for basedir in config['basedirs']:
-        rc = do_stop(basedir, stdout, stderr) or rc
-    if rc == 2 and config['force']:
+    rc = stop(config, stdout, stderr)
+    if rc == 2:
         print >>stderr, "ignoring couldn't-stop"
         rc = 0
     if rc:
         print >>stderr, "not restarting"
         return rc
-    for basedir in config['basedirs']:
-        rc = do_start(basedir, config['profile'], stdout, stderr) or rc
-    return rc
+    return start(config, stdout, stderr)
 
 def run(config, stdout, stderr):
-    from twisted.internet import reactor
-    from twisted.python import log, logfile
-    from allmydata import client
+    config.twistd_args = config.twistd_args + ("--nodaemon",)
+    # Previously we would do the equivalent of adding ("--logfile",
+    # "tahoesvc.log"), but that redirects stdout/stderr which is often
+    # unhelpful, and the user can add that option explicitly if they want.
 
-    basedir = config['basedir']
-    if basedir is None:
-        basedir = '.'
-    else:
-        os.chdir(basedir)
-
-    # set up twisted logging. this will become part of the node rsn.
-    logdir = os.path.join(basedir, 'logs')
-    if not os.path.exists(logdir):
-        os.makedirs(logdir)
-    lf = logfile.LogFile('tahoesvc.log', logdir)
-    log.startLogging(lf)
-
-    # run the node itself
-    c = client.Client(basedir)
-    reactor.callLater(0, c.startService) # after reactor startup
-    reactor.run()
-
-    return 0
+    return start(config, stdout, stderr)
 
 
 subCommands = [