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

Last change on this file was 53084f7, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-27T23:49:07Z

remove more Python2 compatibility

  • Property mode set to 100644
File size: 11.9 KB
Line 
1import os, sys
2from io import StringIO
3import six
4
5from twisted.python import usage
6from twisted.internet import defer, task, threads
7
8from allmydata.scripts.common import get_default_nodedir
9from allmydata.scripts import debug, create_node, cli, \
10    admin, tahoe_run, tahoe_invite
11from allmydata.scripts.types_ import SubCommands
12from allmydata.util.encodingutil import quote_local_unicode_path, argv_to_unicode
13from allmydata.util.eliotutil import (
14    opt_eliot_destination,
15    opt_help_eliot_destinations,
16    eliot_logging_service,
17)
18
19from .. import (
20    __full_version__,
21)
22
23_default_nodedir = get_default_nodedir()
24
25NODEDIR_HELP = ("Specify which Tahoe node directory should be used. The "
26                "directory should either contain a full Tahoe node, or a "
27                "file named node.url that points to some other Tahoe node. "
28                "It should also contain a file named '"
29                + os.path.join('private', 'aliases') +
30                "' which contains the mapping from alias name to root "
31                "dirnode URI.")
32if _default_nodedir:
33    NODEDIR_HELP += " [default for most commands: " + quote_local_unicode_path(_default_nodedir) + "]"
34
35
36process_control_commands : SubCommands = [
37    ("run", None, tahoe_run.RunOptions, "run a node without daemonizing"),
38]
39
40
41class Options(usage.Options):
42    """
43    :ivar wormhole: An object exposing the magic-wormhole API (mainly a test
44        hook).
45    """
46    # unit tests can override these to point at StringIO instances
47    stdin = sys.stdin
48    stdout = sys.stdout
49    stderr = sys.stderr
50
51    from wormhole import wormhole
52
53    subCommands = (     create_node.subCommands
54                    +   admin.subCommands
55                    +   process_control_commands
56                    +   debug.subCommands
57                    +   cli.subCommands
58                    +   tahoe_invite.subCommands
59                    )
60
61    optFlags = [
62        ["quiet", "q", "Operate silently."],
63        ["version", "V", "Display version numbers."],
64        ["version-and-path", None, "Display version numbers and paths to their locations."],
65    ]
66    optParameters = [
67        ["node-directory", "d", None, NODEDIR_HELP],
68        ["wormhole-server", None, u"ws://wormhole.tahoe-lafs.org:4000/v1", "The magic wormhole server to use.", str],
69        ["wormhole-invite-appid", None, u"tahoe-lafs.org/invite", "The appid to use on the wormhole server.", str],
70    ]
71
72    def opt_version(self):
73        print(__full_version__, file=self.stdout)
74        self.no_command_needed = True
75
76    opt_version_and_path = opt_version
77
78    opt_eliot_destination = opt_eliot_destination
79    opt_help_eliot_destinations = opt_help_eliot_destinations
80
81    def __str__(self):
82        return ("\nUsage: tahoe [global-options] <command> [command-options]\n"
83                + self.getUsage())
84
85    synopsis = "\nUsage: tahoe [global-options]" # used only for subcommands
86
87    def getUsage(self, **kwargs):
88        t = usage.Options.getUsage(self, **kwargs)
89        t = t.replace("Options:", "\nGlobal options:", 1)
90        return t + "\nPlease run 'tahoe <command> --help' for more details on each command.\n"
91
92    def postOptions(self):
93        if not hasattr(self, 'subOptions'):
94            if not hasattr(self, 'no_command_needed'):
95                raise usage.UsageError("must specify a command")
96            sys.exit(0)
97
98
99create_dispatch = {}
100for module in (create_node,):
101    create_dispatch.update(module.dispatch)  # type: ignore
102
103def parse_options(argv, config=None):
104    if not config:
105        config = Options()
106    try:
107        config.parseOptions(argv)
108    except usage.error:
109        raise
110    return config
111
112def parse_or_exit(config, argv, stdout, stderr):
113    """
114    Parse Tahoe-LAFS CLI arguments and return a configuration object if they
115    are valid.
116
117    If they are invalid, write an explanation to ``stdout`` and exit.
118
119    :param allmydata.scripts.runner.Options config: An instance of the
120        argument-parsing class to use.
121
122    :param [unicode] argv: The argument list to parse, including the name of the
123        program being run as ``argv[0]``.
124
125    :param stdout: The file-like object to use as stdout.
126    :param stderr: The file-like object to use as stderr.
127
128    :raise SystemExit: If there is an argument-parsing problem.
129
130    :return: ``config``, after using it to parse the argument list.
131    """
132    try:
133        config.stdout = stdout
134        config.stderr = stderr
135        parse_options(argv[1:], config=config)
136    except usage.error as e:
137        # `parse_options` may have the side-effect of initializing a
138        # "sub-option" of the given configuration, even if it ultimately
139        # raises an exception.  For example, `tahoe run --invalid-option` will
140        # set `config.subOptions` to an instance of
141        # `allmydata.scripts.tahoe_run.RunOptions` and then raise a
142        # `usage.error` because `RunOptions` does not recognize
143        # `--invalid-option`.  If `run` itself had a sub-options then the same
144        # thing could happen but with another layer of nesting.  We can
145        # present the user with the most precise information about their usage
146        # error possible by finding the most "sub" of the sub-options and then
147        # showing that to the user along with the usage error.
148        c = config
149        while hasattr(c, 'subOptions'):
150            c = c.subOptions
151        print(str(c), file=stdout)
152        exc_str = str(e)
153        exc_bytes = six.ensure_binary(exc_str, "utf-8")
154        msg_bytes = b"%s%s\n" % (six.ensure_binary(argv[0]), exc_bytes)
155        print(six.ensure_text(msg_bytes, "utf-8"), file=stdout)
156        sys.exit(1)
157    return config
158
159def dispatch(config,
160             reactor,
161             stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr):
162    command = config.subCommand
163    so = config.subOptions
164    if config['quiet']:
165        stdout = StringIO()
166    so.stdout = stdout
167    so.stderr = stderr
168    so.stdin = stdin
169    config.stdin = stdin
170
171    if command in create_dispatch:
172        f = create_dispatch[command]
173    elif command == "run":
174        f = lambda config: tahoe_run.run(reactor, config)
175    elif command in debug.dispatch:
176        f = debug.dispatch[command]
177    elif command in admin.dispatch:
178        f = admin.dispatch[command]
179    elif command in cli.dispatch:
180        # these are blocking, and must be run in a thread
181        f0 = cli.dispatch[command]
182        f = lambda so: threads.deferToThread(f0, so)
183    elif command in tahoe_invite.dispatch:
184        f = tahoe_invite.dispatch[command]
185    else:
186        raise usage.UsageError()
187
188    d = defer.maybeDeferred(f, so)
189    # the calling convention for CLI dispatch functions is that they either:
190    # 1: succeed and return rc=0
191    # 2: print explanation to stderr and return rc!=0
192    # 3: raise an exception that should just be printed normally
193    # 4: return a Deferred that does 1 or 2 or 3
194    def _raise_sys_exit(rc):
195        sys.exit(rc)
196    d.addCallback(_raise_sys_exit)
197    return d
198
199def _maybe_enable_eliot_logging(options, reactor):
200    if options.get("destinations"):
201        service = eliot_logging_service(reactor, options["destinations"])
202        # There is no Twisted "Application" around to hang this on so start
203        # and stop it ourselves.
204        service.startService()
205        reactor.addSystemEventTrigger("after", "shutdown", service.stopService)
206    # Pass on the options so we can dispatch the subcommand.
207    return options
208
209
210def run(configFactory=Options, argv=sys.argv, stdout=sys.stdout, stderr=sys.stderr):
211    """
212    Run a Tahoe-LAFS node.
213
214    :param configFactory: A zero-argument callable which creates the config
215        object to use to parse the argument list.
216
217    :param [str] argv: The argument list to use to configure the run.
218
219    :param stdout: The file-like object to use for stdout.
220    :param stderr: The file-like object to use for stderr.
221
222    :raise SystemExit: Always raised after the run is complete.
223    """
224    if sys.platform == "win32":
225        from allmydata.windows.fixups import initialize
226        initialize()
227    # doesn't return: calls sys.exit(rc)
228    task.react(
229        lambda reactor: _run_with_reactor(
230            reactor,
231            configFactory(),
232            argv,
233            stdout,
234            stderr,
235        ),
236    )
237
238
239def _setup_coverage(reactor, argv):
240    """
241    If coverage measurement was requested, start collecting coverage
242    measurements and arrange to record those measurements when the process is
243    done.
244
245    Coverage measurement is considered requested if ``"--coverage"`` is in
246    ``argv`` (and it will be removed from ``argv`` if it is found).  There
247    should be a ``.coveragerc`` file in the working directory if coverage
248    measurement is requested.
249
250    This is only necessary to support multi-process coverage measurement,
251    typically when the test suite is running, and with the pytest-based
252    *integration* test suite (at ``integration/`` in the root of the source
253    tree) foremost in mind.  The idea is that if you are running Tahoe-LAFS in
254    a configuration where multiple processes are involved - for example, a
255    test process and a client node process, if you only measure coverage from
256    the test process then you will fail to observe most Tahoe-LAFS code that
257    is being run.
258
259    This function arranges to have any Tahoe-LAFS process (such as that
260    client node process) collect and report coverage measurements as well.
261    """
262    # can we put this _setup_coverage call after we hit
263    # argument-parsing?
264    # ensure_str() only necessary on Python 2.
265    if '--coverage' not in sys.argv:
266        return
267    argv.remove('--coverage')
268
269    try:
270        import coverage
271    except ImportError:
272        raise RuntimeError(
273                "The 'coveage' package must be installed to use --coverage"
274        )
275
276    # this doesn't change the shell's notion of the environment, but
277    # it makes the test in process_startup() succeed, which is the
278    # goal here.
279    os.environ["COVERAGE_PROCESS_START"] = '.coveragerc'
280
281    # maybe-start the global coverage, unless it already got started
282    cov = coverage.process_startup()
283    if cov is None:
284        cov = coverage.process_startup.coverage
285
286    def write_coverage_data():
287        """
288        Make sure that coverage has stopped; internally, it depends on
289        ataxit handlers running which doesn't always happen (Twisted's
290        shutdown hook also won't run if os._exit() is called, but it
291        runs more-often than atexit handlers).
292        """
293        cov.stop()
294        cov.save()
295    reactor.addSystemEventTrigger('after', 'shutdown', write_coverage_data)
296
297
298def _run_with_reactor(reactor, config, argv, stdout, stderr):
299    """
300    Run a Tahoe-LAFS node using the given reactor.
301
302    :param reactor: The reactor to use.  This implementation largely ignores
303        this and lets the rest of the implementation pick its own reactor.
304        Oops.
305
306    :param twisted.python.usage.Options config: The config object to use to
307        parse the argument list.
308
309    :param [str] argv: The argument list to parse, *excluding* the name of the
310        program being run.
311
312    :param stdout: See ``run``.
313    :param stderr: See ``run``.
314
315    :return: A ``Deferred`` that fires when the run is complete.
316    """
317    _setup_coverage(reactor, argv)
318
319    argv = list(map(argv_to_unicode, argv))
320    d = defer.maybeDeferred(
321        parse_or_exit,
322        config,
323        argv,
324        stdout,
325        stderr,
326    )
327    d.addCallback(_maybe_enable_eliot_logging, reactor)
328    d.addCallback(dispatch, reactor, stdout=stdout, stderr=stderr)
329    def _show_exception(f):
330        # when task.react() notices a non-SystemExit exception, it does
331        # log.err() with the failure and then exits with rc=1. We want this
332        # to actually print the exception to stderr, like it would do if we
333        # weren't using react().
334        if f.check(SystemExit):
335            return f # dispatch function handled it
336        f.printTraceback(file=stderr)
337        sys.exit(1)
338    d.addErrback(_show_exception)
339    return d
340
341if __name__ == "__main__":
342    run()
Note: See TracBrowser for help on using the repository browser.