diff --git a/src/allmydata/scripts/startstop_node.py b/src/allmydata/scripts/startstop_node.py index 5045bd6..ecf67ae 100644 --- a/src/allmydata/scripts/startstop_node.py +++ b/src/allmydata/scripts/startstop_node.py @@ -1,16 +1,22 @@ import os, sys, signal, time +from twisted.scripts import twistd +from twisted.python import usage from allmydata.scripts.common import BasedirMixin, BaseOptions from allmydata.util import fileutil -from allmydata.util.assertutil import precondition from allmydata.util.encodingutil import listdir_unicode, quote_output class StartOptions(BasedirMixin, BaseOptions): - optFlags = [ - ["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 parseArgs(self, basedir=None, *twistd_args): + # this can't handle e.g. 'tahoe start --nodaemon', since then + # --nodaemon looks like a basedir. So you can either use 'tahoe + # start' or 'tahoe start BASEDIR --TWISTD-OPTIONS'. + BasedirMixin.parseArgs(self, basedir) + if not os.path.isdir(self['basedir']): + raise usage.UsageError("--basedir '%s' doesn't exist" % + quote_output(self['basedir'])) + self.twistd_args = twistd_args def getSynopsis(self): return "Usage: %s start [options] [NODEDIR]" % (self.command_name,) @@ -21,63 +27,121 @@ class StopOptions(BasedirMixin, BaseOptions): return "Usage: %s stop [options] [NODEDIR]" % (self.command_name,) -class RestartOptions(BasedirMixin, BaseOptions): - optFlags = [ - ["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 RestartOptions(StartOptions): def getSynopsis(self): return "Usage: %s restart [options] [NODEDIR]" % (self.command_name,) - -class RunOptions(BasedirMixin, BaseOptions): - default_nodedir = u"." - - optParameters = [ - ["node-directory", "d", None, "Specify the directory of the node to be run. [default, for 'tahoe run' only: current directory]"], - ] - +class RunOptions(StartOptions): def getSynopsis(self): return "Usage: %s 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 +class MyTwistdConfig(twistd.ServerOptions): + subCommands = [("XYZ", None, usage.Options, "node")] + +class NodeStartingPlugin: + tapname = "xyznode" + 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 (no .tac file)" % quote_output(basedir) - return 1 + return None if "client" in tac: - nodetype = "client" + return "client" elif "introducer" in tac: - nodetype = "introducer" + return "introducer" + elif "key-generator" in tac: + return "key-generator" + elif "stats-gatherer" in tac: + return "stats-gatherer" else: - nodetype = "unknown (%s)" % tac + return None - 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 +def start(config, out=sys.stdout, err=sys.stderr): + basedir = config['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 + nodetype = identify_node_type(basedir) + if not nodetype: + print >>err, "%s is not a recognizable node directory" % quote_output(basedir) + return 1 + # Now prepare to turn into a twistd process. This os.chdir is the point + # of no return. 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. + 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, "logs")) + twistd_args.extend(["--logfile", os.path.join("logs", "twistd.log")]) + twistd_args.extend(config.twistd_args) + twistd_args.append("XYZ") # point at our NodeStartingPlugin + + twistd_config = MyTwistdConfig() + try: + twistd_config.parseOptions(twistd_args) + except usage.error, ue: + # these arguments were unsuitable for 'twistd' + print >>err, twistd_config + print >>err, "tahoe start: %s" % (config.subCommand, ue) + return 1 + twistd_config.loadedPlugins = {"XYZ": NodeStartingPlugin(nodetype, basedir)} + + # 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. + + verb = "starting" + if "--nodaemon" in twistd_args: + verb = "running" + print >>out, "%s node in %s" % (verb, basedir) + twistd.runApp(twistd_config) + # we should only reach here if --nodaemon was used + return 0 def stop(config, out=sys.stdout, err=sys.stderr): basedir = config['basedir'] @@ -147,44 +211,9 @@ def restart(config, stdout, stderr): 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 + config.twistd_args = config.twistd_args + ("--nodaemon",) + # also ("--logfile", "tahoesvc.log") + return start(config, stdout, stderr) subCommands = [ diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index 4e9f683..53f8850 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -664,7 +664,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin, def _cb(res): out, err, rc_or_sig = res self.failUnlessEqual(rc_or_sig, 1) - self.failUnless("does not look like a node directory" in err, err) + self.failUnless("is not a recognizable node directory" in err, err) d.addCallback(_cb) def _then_stop_it(res): @@ -685,7 +685,8 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin, def _cb3(res): out, err, rc_or_sig = res self.failUnlessEqual(rc_or_sig, 1) - self.failUnless("does not look like a directory at all" in err, err) + self.failUnless("--basedir" in out, out) + self.failUnless("doesn't exist" in out, out) d.addCallback(_cb3) return d