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