]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/scripts/startstop_node.py
Merge branch '1159-notac-4'
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / scripts / startstop_node.py
1
2 import os, sys, signal, time
3 from allmydata.scripts.common import BasedirOptions
4 from twisted.scripts import twistd
5 from twisted.python import usage
6 from allmydata.util import fileutil
7 from allmydata.util.encodingutil import listdir_unicode, quote_output
8
9
10 class StartOptions(BasedirOptions):
11     subcommand_name = "start"
12
13     def parseArgs(self, basedir=None, *twistd_args):
14         # This can't handle e.g. 'tahoe start --nodaemon', since '--nodaemon'
15         # looks like an option to the tahoe subcommand, not to twistd.
16         # So you can either use 'tahoe start' or 'tahoe start NODEDIR --TWISTD-OPTIONS'.
17         # Note that 'tahoe --node-directory=NODEDIR start --TWISTD-OPTIONS' also
18         # isn't allowed, unfortunately.
19
20         BasedirOptions.parseArgs(self, basedir)
21         self.twistd_args = twistd_args
22
23     def getSynopsis(self):
24         return "Usage:  %s [global-opts] %s [options] [NODEDIR [twistd-options]]" % (self.command_name, self.subcommand_name)
25
26     def getUsage(self, width=None):
27         t = BasedirOptions.getUsage(self, width) + "\n"
28         twistd_options = str(MyTwistdConfig()).partition("\n")[2].partition("\n\n")[0]
29         t += twistd_options.replace("Options:", "twistd-options:", 1)
30         t += """
31
32 Note that if any twistd-options are used, NODEDIR must be specified explicitly
33 (not by default or using -C/--basedir or -d/--node-directory), and followed by
34 the twistd-options.
35 """
36         return t
37
38 class StopOptions(BasedirOptions):
39     def parseArgs(self, basedir=None):
40         BasedirOptions.parseArgs(self, basedir)
41
42     def getSynopsis(self):
43         return "Usage:  %s [global-opts] stop [options] [NODEDIR]" % (self.command_name,)
44
45 class RestartOptions(StartOptions):
46     subcommand_name = "restart"
47
48 class RunOptions(StartOptions):
49     subcommand_name = "run"
50
51
52 class MyTwistdConfig(twistd.ServerOptions):
53     subCommands = [("StartTahoeNode", None, usage.Options, "node")]
54
55 class StartTahoeNodePlugin:
56     tapname = "tahoenode"
57     def __init__(self, nodetype, basedir):
58         self.nodetype = nodetype
59         self.basedir = basedir
60     def makeService(self, so):
61         # delay this import as late as possible, to allow twistd's code to
62         # accept --reactor= selection. N.B.: this can't actually work until
63         # this file, and all the __init__.py files above it, also respect the
64         # prohibition on importing anything that transitively imports
65         # twisted.internet.reactor . That will take a lot of work.
66         if self.nodetype == "client":
67             from allmydata.client import Client
68             return Client(self.basedir)
69         if self.nodetype == "introducer":
70             from allmydata.introducer.server import IntroducerNode
71             return IntroducerNode(self.basedir)
72         if self.nodetype == "key-generator":
73             from allmydata.key_generator import KeyGeneratorService
74             return KeyGeneratorService(default_key_size=2048)
75         if self.nodetype == "stats-gatherer":
76             from allmydata.stats import StatsGathererService
77             return StatsGathererService(verbose=True)
78         raise ValueError("unknown nodetype %s" % self.nodetype)
79
80 def identify_node_type(basedir):
81     for fn in listdir_unicode(basedir):
82         if fn.endswith(u".tac"):
83             tac = str(fn)
84             break
85     else:
86         return None
87
88     for t in ("client", "introducer", "key-generator", "stats-gatherer"):
89         if t in tac:
90             return t
91     return None
92
93 def start(config, out=sys.stdout, err=sys.stderr):
94     basedir = config['basedir']
95     print >>out, "STARTING", quote_output(basedir)
96     if not os.path.isdir(basedir):
97         print >>err, "%s does not look like a directory at all" % quote_output(basedir)
98         return 1
99     nodetype = identify_node_type(basedir)
100     if not nodetype:
101         print >>err, "%s is not a recognizable node directory" % quote_output(basedir)
102         return 1
103     # Now prepare to turn into a twistd process. This os.chdir is the point
104     # of no return.
105     os.chdir(basedir)
106     twistd_args = []
107     if (nodetype in ("client", "introducer")
108         and "--nodaemon" not in config.twistd_args
109         and "--syslog" not in config.twistd_args
110         and "--logfile" not in config.twistd_args):
111         fileutil.make_dirs(os.path.join(basedir, "logs"))
112         twistd_args.extend(["--logfile", os.path.join("logs", "twistd.log")])
113     twistd_args.extend(config.twistd_args)
114     twistd_args.append("StartTahoeNode") # point at our StartTahoeNodePlugin
115
116     twistd_config = MyTwistdConfig()
117     try:
118         twistd_config.parseOptions(twistd_args)
119     except usage.error, ue:
120         # these arguments were unsuitable for 'twistd'
121         print >>err, config
122         print >>err, "tahoe %s: usage error from twistd: %s\n" % (config.subcommand_name, ue)
123         return 1
124     twistd_config.loadedPlugins = {"StartTahoeNode": StartTahoeNodePlugin(nodetype, basedir)}
125
126     # On Unix-like platforms:
127     #   Unless --nodaemon was provided, the twistd.runApp() below spawns off a
128     #   child process, and the parent calls os._exit(0), so there's no way for
129     #   us to get control afterwards, even with 'except SystemExit'. If
130     #   application setup fails (e.g. ImportError), runApp() will raise an
131     #   exception.
132     #
133     #   So if we wanted to do anything with the running child, we'd have two
134     #   options:
135     #
136     #    * fork first, and have our child wait for the runApp() child to get
137     #      running. (note: just fork(). This is easier than fork+exec, since we
138     #      don't have to get PATH and PYTHONPATH set up, since we're not
139     #      starting a *different* process, just cloning a new instance of the
140     #      current process)
141     #    * or have the user run a separate command some time after this one
142     #      exits.
143     #
144     #   For Tahoe, we don't need to do anything with the child, so we can just
145     #   let it exit.
146     #
147     # On Windows:
148     #   twistd does not fork; it just runs in the current process whether or not
149     #   --nodaemon is specified. (As on Unix, --nodaemon does have the side effect
150     #   of causing us to log to stdout/stderr.)
151
152     if "--nodaemon" in twistd_args or sys.platform == "win32":
153         verb = "running"
154     else:
155         verb = "starting"
156
157     print >>out, "%s node in %s" % (verb, basedir)
158     twistd.runApp(twistd_config)
159     # we should only reach here if --nodaemon or equivalent was used
160     return 0
161
162 def stop(config, out=sys.stdout, err=sys.stderr):
163     basedir = config['basedir']
164     print >>out, "STOPPING", quote_output(basedir)
165     pidfile = os.path.join(basedir, "twistd.pid")
166     if not os.path.exists(pidfile):
167         print >>err, "%s does not look like a running node directory (no twistd.pid)" % quote_output(basedir)
168         # we define rc=2 to mean "nothing is running, but it wasn't me who
169         # stopped it"
170         return 2
171     pid = open(pidfile, "r").read()
172     pid = int(pid)
173
174     # kill it hard (SIGKILL), delete the twistd.pid file, then wait for the
175     # process itself to go away. If it hasn't gone away after 20 seconds, warn
176     # the user but keep waiting until they give up.
177     try:
178         os.kill(pid, signal.SIGKILL)
179     except OSError, oserr:
180         if oserr.errno == 3:
181             print oserr.strerror
182             # the process didn't exist, so wipe the pid file
183             os.remove(pidfile)
184             return 2
185         else:
186             raise
187     try:
188         os.remove(pidfile)
189     except EnvironmentError:
190         pass
191     start = time.time()
192     time.sleep(0.1)
193     wait = 40
194     first_time = True
195     while True:
196         # poll once per second until we see the process is no longer running
197         try:
198             os.kill(pid, 0)
199         except OSError:
200             print >>out, "process %d is dead" % pid
201             return
202         wait -= 1
203         if wait < 0:
204             if first_time:
205                 print >>err, ("It looks like pid %d is still running "
206                               "after %d seconds" % (pid,
207                                                     (time.time() - start)))
208                 print >>err, "I will keep watching it until you interrupt me."
209                 wait = 10
210                 first_time = False
211             else:
212                 print >>err, "pid %d still running after %d seconds" % \
213                       (pid, (time.time() - start))
214                 wait = 10
215         time.sleep(1)
216     # we define rc=1 to mean "I think something is still running, sorry"
217     return 1
218
219 def restart(config, stdout, stderr):
220     rc = stop(config, stdout, stderr)
221     if rc == 2:
222         print >>stderr, "ignoring couldn't-stop"
223         rc = 0
224     if rc:
225         print >>stderr, "not restarting"
226         return rc
227     return start(config, stdout, stderr)
228
229 def run(config, stdout, stderr):
230     config.twistd_args = config.twistd_args + ("--nodaemon",)
231     # Previously we would do the equivalent of adding ("--logfile", "tahoesvc.log"),
232     # but that redirects stdout/stderr which is often unhelpful, and the user can
233     # add that option explicitly if they want.
234
235     return start(config, stdout, stderr)
236
237
238 subCommands = [
239     ["start", None, StartOptions, "Start a node (of any type)."],
240     ["stop", None, StopOptions, "Stop a node."],
241     ["restart", None, RestartOptions, "Restart a node."],
242     ["run", None, RunOptions, "Run a node synchronously."],
243 ]
244
245 dispatch = {
246     "start": start,
247     "stop": stop,
248     "restart": restart,
249     "run": run,
250     }