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