source: trunk/src/allmydata/scripts/tahoe_run.py

Last change on this file was 1cfe843d, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-22T23:40:25Z

more python2 removal

  • Property mode set to 100644
File size: 11.5 KB
Line 
1"""
2Ported to Python 3.
3"""
4
5__all__ = [
6    "RunOptions",
7    "run",
8]
9
10import os, sys
11from allmydata.scripts.common import BasedirOptions
12from twisted.scripts import twistd
13from twisted.python import usage
14from twisted.python.filepath import FilePath
15from twisted.python.reflect import namedAny
16from twisted.python.failure import Failure
17from twisted.internet.defer import maybeDeferred, Deferred
18from twisted.internet.protocol import Protocol
19from twisted.internet.stdio import StandardIO
20from twisted.internet.error import ReactorNotRunning
21from twisted.application.service import Service
22
23from allmydata.scripts.default_nodedir import _default_nodedir
24from allmydata.util.encodingutil import listdir_unicode, quote_local_unicode_path
25from allmydata.util.configutil import UnknownConfigError
26from allmydata.util.deferredutil import HookMixin
27from allmydata.util.pid import (
28    parse_pidfile,
29    check_pid_process,
30    cleanup_pidfile,
31    ProcessInTheWay,
32    InvalidPidFile,
33)
34from allmydata.storage.crawler import (
35    MigratePickleFileError,
36)
37from allmydata.storage_client import (
38    MissingPlugin,
39)
40from allmydata.node import (
41    PortAssignmentRequired,
42    PrivacyError,
43)
44
45
46def get_pidfile(basedir):
47    """
48    Returns the path to the PID file.
49    :param basedir: the node's base directory
50    :returns: the path to the PID file
51    """
52    return os.path.join(basedir, u"running.process")
53
54
55def get_pid_from_pidfile(pidfile):
56    """
57    Tries to read and return the PID stored in the node's PID file
58
59    :param pidfile: try to read this PID file
60    :returns: A numeric PID on success, ``None`` if PID file absent or
61              inaccessible, ``-1`` if PID file invalid.
62    """
63    try:
64        pid, _ = parse_pidfile(pidfile)
65    except EnvironmentError:
66        return None
67    except InvalidPidFile:
68        return -1
69
70    return pid
71
72
73def identify_node_type(basedir):
74    """
75    :return unicode: None or one of: 'client' or 'introducer'.
76    """
77    tac = u''
78    try:
79        for fn in listdir_unicode(basedir):
80            if fn.endswith(u".tac"):
81                tac = fn
82                break
83    except OSError:
84        return None
85
86    for t in (u"client", u"introducer"):
87        if t in tac:
88            return t
89    return None
90
91
92class RunOptions(BasedirOptions):
93    subcommand_name = "run"
94
95    optParameters = [
96        ("basedir", "C", None,
97         "Specify which Tahoe base directory should be used."
98         " This has the same effect as the global --node-directory option."
99         " [default: %s]" % quote_local_unicode_path(_default_nodedir)),
100        ]
101
102    optFlags = [
103        ("allow-stdin-close", None,
104         'Do not exit when stdin closes ("tahoe run" otherwise will exit).'),
105    ]
106
107    def parseArgs(self, basedir=None, *twistd_args):
108        # This can't handle e.g. 'tahoe run --reactor=foo', since
109        # '--reactor=foo' looks like an option to the tahoe subcommand, not to
110        # twistd. So you can either use 'tahoe run' or 'tahoe run NODEDIR
111        # --TWISTD-OPTIONS'. Note that 'tahoe --node-directory=NODEDIR run
112        # --TWISTD-OPTIONS' also isn't allowed, unfortunately.
113
114        BasedirOptions.parseArgs(self, basedir)
115        self.twistd_args = twistd_args
116
117    def getSynopsis(self):
118        return ("Usage:  %s [global-options] %s [options]"
119                " [NODEDIR [twistd-options]]"
120                % (self.command_name, self.subcommand_name))
121
122    def getUsage(self, width=None):
123        t = BasedirOptions.getUsage(self, width) + "\n"
124        twistd_options = str(MyTwistdConfig()).partition("\n")[2].partition("\n\n")[0]
125        t += twistd_options.replace("Options:", "twistd-options:", 1)
126        t += """
127
128Note that if any twistd-options are used, NODEDIR must be specified explicitly
129(not by default or using -C/--basedir or -d/--node-directory), and followed by
130the twistd-options.
131"""
132        return t
133
134
135class MyTwistdConfig(twistd.ServerOptions):
136    subCommands = [("DaemonizeTahoeNode", None, usage.Options, "node")]
137
138    stderr = sys.stderr
139
140
141class DaemonizeTheRealService(Service, HookMixin):
142    """
143    this HookMixin should really be a helper; our hooks:
144
145    - 'running': triggered when startup has completed; it triggers
146        with None of successful or a Failure otherwise.
147    """
148    stderr = sys.stderr
149
150    def __init__(self, nodetype, basedir, options):
151        super(DaemonizeTheRealService, self).__init__()
152        self.nodetype = nodetype
153        self.basedir = basedir
154        # setup for HookMixin
155        self._hooks = {
156            "running": None,
157        }
158        self.stderr = options.parent.stderr
159        self._close_on_stdin_close = False if options["allow-stdin-close"] else True
160
161    def startService(self):
162
163        from twisted.internet import reactor
164
165        def start():
166            node_to_instance = {
167                u"client": lambda: maybeDeferred(namedAny("allmydata.client.create_client"), self.basedir),
168                u"introducer": lambda: maybeDeferred(namedAny("allmydata.introducer.server.create_introducer"), self.basedir),
169            }
170
171            try:
172                service_factory = node_to_instance[self.nodetype]
173            except KeyError:
174                raise ValueError("unknown nodetype %s" % self.nodetype)
175
176            def handle_config_error(reason):
177                if reason.check(UnknownConfigError):
178                    self.stderr.write("\nConfiguration error:\n{}\n\n".format(reason.value))
179                elif reason.check(PortAssignmentRequired):
180                    self.stderr.write("\ntub.port cannot be 0: you must choose.\n\n")
181                elif reason.check(PrivacyError):
182                    self.stderr.write("\n{}\n\n".format(reason.value))
183                elif reason.check(MigratePickleFileError):
184                    self.stderr.write(
185                        "Error\nAt least one 'pickle' format file exists.\n"
186                        "The file is {}\n"
187                        "You must either delete the pickle-format files"
188                        " or migrate them using the command:\n"
189                        "    tahoe admin migrate-crawler --basedir {}\n\n"
190                        .format(
191                            reason.value.args[0].path,
192                            self.basedir,
193                        )
194                    )
195                elif reason.check(MissingPlugin):
196                    self.stderr.write(
197                        "Missing Plugin\n"
198                        "The configuration requests a plugin:\n"
199                        "\n    {}\n\n"
200                        "...which cannot be found.\n"
201                        "This typically means that some software hasn't been installed or the plugin couldn't be instantiated.\n\n"
202                        .format(
203                            reason.value.plugin_name,
204                        )
205                    )
206                else:
207                    self.stderr.write("\nUnknown error, here's the traceback:\n")
208                    reason.printTraceback(self.stderr)
209                reactor.stop()
210
211            d = service_factory()
212
213            def created(srv):
214                if self.parent is not None:
215                    srv.setServiceParent(self.parent)
216                # exiting on stdin-closed facilitates cleanup when run
217                # as a subprocess
218                if self._close_on_stdin_close:
219                    on_stdin_close(reactor, reactor.stop)
220            d.addCallback(created)
221            d.addErrback(handle_config_error)
222            d.addBoth(self._call_hook, 'running')
223            return d
224
225        reactor.callWhenRunning(start)
226
227
228class DaemonizeTahoeNodePlugin(object):
229    tapname = "tahoenode"
230    def __init__(self, nodetype, basedir, allow_stdin_close):
231        self.nodetype = nodetype
232        self.basedir = basedir
233        self.allow_stdin_close = allow_stdin_close
234
235    def makeService(self, so):
236        so["allow-stdin-close"] = self.allow_stdin_close
237        return DaemonizeTheRealService(self.nodetype, self.basedir, so)
238
239
240def on_stdin_close(reactor, fn):
241    """
242    Arrange for the function `fn` to run when our stdin closes
243    """
244    when_closed_d = Deferred()
245
246    class WhenClosed(Protocol):
247        """
248        Notify a Deferred when our connection is lost .. as this is passed
249        to twisted's StandardIO class, it is used to detect our parent
250        going away.
251        """
252
253        def connectionLost(self, reason):
254            when_closed_d.callback(None)
255
256    def on_close(arg):
257        try:
258            fn()
259        except ReactorNotRunning:
260            pass
261        except Exception:
262            # for our "exit" use-case failures will _mostly_ just be
263            # ReactorNotRunning (because we're already shutting down
264            # when our stdin closes) but no matter what "bad thing"
265            # happens we just want to ignore it .. although other
266            # errors might be interesting so we'll log those
267            print(Failure())
268        return arg
269
270    when_closed_d.addBoth(on_close)
271    # we don't need to do anything with this instance because it gets
272    # hooked into the reactor and thus remembered .. but we return it
273    # for Windows testing purposes.
274    return StandardIO(
275        proto=WhenClosed(),
276        reactor=reactor,
277    )
278
279
280def run(reactor, config, runApp=twistd.runApp):
281    """
282    Runs a Tahoe-LAFS node in the foreground.
283
284    Sets up the IService instance corresponding to the type of node
285    that's starting and uses Twisted's twistd runner to disconnect our
286    process from the terminal.
287    """
288    out = config.stdout
289    err = config.stderr
290    basedir = config['basedir']
291    quoted_basedir = quote_local_unicode_path(basedir)
292    print("'tahoe {}' in {}".format(config.subcommand_name, quoted_basedir), file=out)
293    if not os.path.isdir(basedir):
294        print("%s does not look like a directory at all" % quoted_basedir, file=err)
295        return 1
296    nodetype = identify_node_type(basedir)
297    if not nodetype:
298        print("%s is not a recognizable node directory" % quoted_basedir, file=err)
299        return 1
300
301    twistd_args = [
302        # ensure twistd machinery does not daemonize.
303        "--nodaemon",
304        "--rundir", basedir,
305    ]
306    if sys.platform != "win32":
307        # turn off Twisted's pid-file to use our own -- but not on
308        # windows, because twistd doesn't know about pidfiles there
309        twistd_args.extend(["--pidfile", None])
310    twistd_args.extend(config.twistd_args)
311    twistd_args.append("DaemonizeTahoeNode") # point at our DaemonizeTahoeNodePlugin
312
313    twistd_config = MyTwistdConfig()
314    twistd_config.stdout = out
315    twistd_config.stderr = err
316    try:
317        twistd_config.parseOptions(twistd_args)
318    except usage.error as ue:
319        # these arguments were unsuitable for 'twistd'
320        print(config, file=err)
321        print("tahoe %s: usage error from twistd: %s\n" % (config.subcommand_name, ue), file=err)
322        return 1
323    twistd_config.loadedPlugins = {
324        "DaemonizeTahoeNode": DaemonizeTahoeNodePlugin(nodetype, basedir, config["allow-stdin-close"])
325    }
326
327    # our own pid-style file contains PID and process creation time
328    pidfile = FilePath(get_pidfile(config['basedir']))
329    try:
330        check_pid_process(pidfile)
331    except (ProcessInTheWay, InvalidPidFile) as e:
332        print("ERROR: {}".format(e), file=err)
333        return 1
334    else:
335        reactor.addSystemEventTrigger(
336            "after", "shutdown",
337            lambda: cleanup_pidfile(pidfile)
338        )
339
340    # We always pass --nodaemon so twistd.runApp does not daemonize.
341    print("running node in %s" % (quoted_basedir,), file=out)
342    runApp(twistd_config)
343    return 0
Note: See TracBrowser for help on using the repository browser.