]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/scripts/startstop_node.py
CLI: put "[global-opts]" in all command synopses
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / startstop_node.py
index 3988ec3e29dc8eced6d60905bab635dbf57fd922..9ecbf06924f739b7a8491face29352dd874a1491 100644 (file)
 
 import os, sys, signal, time
-from twisted.python import usage
-from allmydata.scripts.common import BasedirMixin
+from allmydata.scripts.common import BasedirOptions
 from allmydata.util import fileutil
-from twisted.python.procutils import which
+from allmydata.util.assertutil import precondition
+from allmydata.util.encodingutil import listdir_unicode, quote_output
 
-class StartOptions(BasedirMixin, usage.Options):
-    optParameters = [
-        ["basedir", "C", None, "which directory to start the node in"],
-        ]
+
+class StartOptions(BasedirOptions):
     optFlags = [
-        ["profile", "p", "whether to run under the Python profiler, putting results in \"profiling_results.prof\""],
+        ["profile", "p", "Run under the Python profiler, putting results in 'profiling_results.prof'."],
+        ["syslog", None, "Tell the node to log to syslog, not a file."],
         ]
 
-class StopOptions(BasedirMixin, usage.Options):
-    optParameters = [
-        ["basedir", "C", None, "which directory to stop the node in"],
-        ]
+    def getSynopsis(self):
+        return "Usage:  %s [global-opts] start [options] [NODEDIR]" % (self.command_name,)
 
-class RestartOptions(BasedirMixin, usage.Options):
-    optParameters = [
-        ["basedir", "C", None, "which directory to restart the node in"],
-        ]
+
+class StopOptions(BasedirOptions):
+    def getSynopsis(self):
+        return "Usage:  %s [global-opts] stop [options] [NODEDIR]" % (self.command_name,)
+
+
+class RestartOptions(BasedirOptions):
     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", "Run under the Python profiler, putting results in 'profiling_results.prof'."],
+        ["syslog", None, "Tell the node to log to syslog, not a file."],
         ]
 
-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, "introducer.tac")):
-        tac = "introducer.tac"
-        type = "introducer"
-    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!"
+    def getSynopsis(self):
+        return "Usage:  %s [global-opts] restart [options] [NODEDIR]" % (self.command_name,)
+
+
+class RunOptions(BasedirOptions):
+    default_nodedir = u"."
+
+    def getSynopsis(self):
+        return "Usage:  %s [global-opts] run [options] [NODEDIR]" % (self.command_name,)
+
+
+def start(opts, out=sys.stdout, err=sys.stderr):
+    basedir = opts['basedir']
+    print >>out, "STARTING", quote_output(basedir)
+    if not os.path.isdir(basedir):
+        print >>err, "%s does not look like a directory at all" % quote_output(basedir)
         return 1
-    twistds = which("twistd")
-    twistd = twistds and twistds[0]
-    if not twistd:
-        twistd = os.path.join(sys.prefix, 'Scripts', 'twistd.py') 
-    if not os.path.exists(twistd):
-        print "Can't find twistd (it comes with Twisted).  Aborting."
-        sys.exit(1)
-    path, ext = os.path.splitext(twistd)
-    if ext.lower() in [".exe", ".bat",]:
-        cmd = [twistd,]
-    else:
-        cmd = [sys.executable, twistd,]
-    
-    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()
-    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
+    for fn in listdir_unicode(basedir):
+        if fn.endswith(u".tac"):
+            tac = str(fn)
+            break
     else:
-        print >>err, "%s node probably not started" % type
+        print >>err, "%s does not look like a node directory (no .tac file)" % quote_output(basedir)
         return 1
+    if "client" in tac:
+        nodetype = "client"
+    elif "introducer" in tac:
+        nodetype = "introducer"
+    else:
+        nodetype = "unknown (%s)" % tac
 
-def do_stop(basedir, out=sys.stdout, err=sys.stderr):
-    print >>out, "STOPPING", basedir
+    args = ["twistd", "-y", tac]
+    if opts["syslog"]:
+        args.append("--syslog")
+    elif nodetype in ("client", "introducer"):
+        fileutil.make_dirs(os.path.join(basedir, "logs"))
+        args.extend(["--logfile", os.path.join("logs", "twistd.log")])
+    if opts["profile"]:
+        args.extend(["--profile=profiling_results.prof", "--savestats",])
+    # now we're committed
+    os.chdir(basedir)
+    from twisted.scripts import twistd
+    sys.argv = args
+    twistd.run()
+    # run() doesn't return: the parent does os._exit(0) in daemonize(), so
+    # we'll never get here. If application setup fails (e.g. ImportError),
+    # run() will raise an exception.
+
+def stop(config, out=sys.stdout, err=sys.stderr):
+    basedir = config['basedir']
+    print >>out, "STOPPING", quote_output(basedir)
     pidfile = os.path.join(basedir, "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)" % quote_output(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)
 
-    timer = 0
+    # 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 20 seconds, warn
+    # the user but keep waiting until they give up.
     try:
-        os.kill(pid, signal.SIGINT)
+        os.kill(pid, signal.SIGKILL)
     except OSError, oserr:
         if oserr.errno == 3:
             print oserr.strerror
-            return 1
+            # the process didn't exist, so wipe the pid file
+            os.remove(pidfile)
+            return 2
         else:
             raise
+    try:
+        os.remove(pidfile)
+    except EnvironmentError:
+        pass
+    start = time.time()
     time.sleep(0.1)
-    while timer < 5:
-        # poll once per second until twistd.pid goes away, up to 5 seconds
+    wait = 40
+    first_time = True
+    while True:
+        # poll once per second until we see the process is no longer running
         try:
             os.kill(pid, 0)
         except OSError:
             print >>out, "process %d is dead" % pid
             return
-        timer += 1
+        wait -= 1
+        if wait < 0:
+            if first_time:
+                print >>err, ("It looks like pid %d is still running "
+                              "after %d seconds" % (pid,
+                                                    (time.time() - start)))
+                print >>err, "I will keep watching it until you interrupt me."
+                wait = 10
+                first_time = False
+            else:
+                print >>err, "pid %d still running after %d seconds" % \
+                      (pid, (time.time() - start))
+                wait = 10
         time.sleep(1)
-    print >>err, "never saw process go away"
+    # 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, 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
+
+    basedir = config['basedir']
+    precondition(isinstance(basedir, unicode), basedir)
+
+    if not os.path.isdir(basedir):
+        print >>stderr, "%s does not look like a directory at all" % quote_output(basedir)
+        return 1
+    for fn in listdir_unicode(basedir):
+        if fn.endswith(u".tac"):
+            tac = str(fn)
+            break
+    else:
+        print >>stderr, "%s does not look like a node directory (no .tac file)" % quote_output(basedir)
+        return 1
+    if "client" not in tac:
+        print >>stderr, ("%s looks like it contains a non-client node (%s).\n"
+                         "Use 'tahoe start' instead of 'tahoe run'."
+                         % (quote_output(basedir), tac))
+        return 1
+
+    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
 
 
 subCommands = [
     ["start", None, StartOptions, "Start a node (of any type)."],
     ["stop", None, StopOptions, "Stop a node."],
     ["restart", None, RestartOptions, "Restart a node."],
+    ["run", None, RunOptions, "Run a node synchronously."],
 ]
 
 dispatch = {
     "start": start,
     "stop": stop,
     "restart": restart,
+    "run": run,
     }