
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
 
