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