1 | import os, sys |
---|
2 | from io import StringIO |
---|
3 | import six |
---|
4 | |
---|
5 | from twisted.python import usage |
---|
6 | from twisted.internet import defer, task, threads |
---|
7 | |
---|
8 | from allmydata.scripts.common import get_default_nodedir |
---|
9 | from allmydata.scripts import debug, create_node, cli, \ |
---|
10 | admin, tahoe_run, tahoe_invite |
---|
11 | from allmydata.scripts.types_ import SubCommands |
---|
12 | from allmydata.util.encodingutil import quote_local_unicode_path, argv_to_unicode |
---|
13 | from allmydata.util.eliotutil import ( |
---|
14 | opt_eliot_destination, |
---|
15 | opt_help_eliot_destinations, |
---|
16 | eliot_logging_service, |
---|
17 | ) |
---|
18 | |
---|
19 | from .. import ( |
---|
20 | __full_version__, |
---|
21 | ) |
---|
22 | |
---|
23 | _default_nodedir = get_default_nodedir() |
---|
24 | |
---|
25 | NODEDIR_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.") |
---|
32 | if _default_nodedir: |
---|
33 | NODEDIR_HELP += " [default for most commands: " + quote_local_unicode_path(_default_nodedir) + "]" |
---|
34 | |
---|
35 | |
---|
36 | process_control_commands : SubCommands = [ |
---|
37 | ("run", None, tahoe_run.RunOptions, "run a node without daemonizing"), |
---|
38 | ] |
---|
39 | |
---|
40 | |
---|
41 | class 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 | |
---|
99 | create_dispatch = {} |
---|
100 | for module in (create_node,): |
---|
101 | create_dispatch.update(module.dispatch) # type: ignore |
---|
102 | |
---|
103 | def 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 | |
---|
112 | def 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 | |
---|
159 | def 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 | |
---|
199 | def _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 | |
---|
210 | def 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 | |
---|
239 | def _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 | |
---|
298 | def _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 | |
---|
341 | if __name__ == "__main__": |
---|
342 | run() |
---|