]> arthur.barton.de Git - bup.git/blob - lib/bup/main.py
bup.main: drop unicode-only getopt
[bup.git] / lib / bup / main.py
1
2 from __future__ import absolute_import, print_function
3
4 import bup_main, os, sys
5 if bup_main.env_pythonpath:
6     if sys.version_info[0] < 3:
7         os.environ['PYTHONPATH'] = bup_main.env_pythonpath
8     else:
9         os.environb[b'PYTHONPATH'] = bup_main.env_pythonpath
10 else:
11     del os.environ['PYTHONPATH']
12
13 from importlib import import_module
14 from pkgutil import iter_modules
15 from subprocess import PIPE
16 from threading import Thread
17 import errno, re, select, signal, subprocess
18
19 from bup import compat, path, helpers
20 from bup.compat import (
21     ModuleNotFoundError,
22     add_ex_ctx,
23     add_ex_tb,
24     argv_bytes,
25     environ,
26     fsdecode,
27     int_types,
28     wrap_main
29 )
30 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
31 from bup.helpers import (
32     columnate,
33     debug1,
34     handle_ctrl_c,
35     log,
36     merge_dict,
37     tty_width
38 )
39 from bup.git import close_catpipes
40 from bup.io import byte_stream, path_msg
41 from bup.options import _tty_width
42 import bup.cmd
43
44 def maybe_import_early(argv):
45     """Scan argv and import any modules specified by --import-py-module."""
46     while argv:
47         if argv[0] != '--import-py-module':
48             argv = argv[1:]
49             continue
50         if len(argv) < 2:
51             log("bup: --import-py-module must have an argument\n")
52             exit(2)
53         mod = argv[1]
54         import_module(mod)
55         argv = argv[2:]
56
57 maybe_import_early(compat.get_argv())
58
59 handle_ctrl_c()
60
61 cmdpath = path.cmddir()
62
63 # We manipulate the subcmds here as strings, but they must be ASCII
64 # compatible, since we're going to be looking for exactly
65 # b'bup-SUBCMD' to exec.
66
67 def usage(msg=""):
68     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
69         '<command> [options...]\n\n')
70     common = dict(
71         ftp = 'Browse backup sets using an ftp-like client',
72         fsck = 'Check backup sets for damage and add redundancy information',
73         fuse = 'Mount your backup sets as a filesystem',
74         help = 'Print detailed help for the given command',
75         index = 'Create or display the index of files to back up',
76         on = 'Backup a remote machine to the local one',
77         restore = 'Extract files from a backup set',
78         save = 'Save files into a backup set (note: run "bup index" first)',
79         tag = 'Tag commits for easier access',
80         web = 'Launch a web server to examine backup sets',
81     )
82
83     log('Common commands:\n')
84     for cmd,synopsis in sorted(common.items()):
85         log('    %-10s %s\n' % (cmd, synopsis))
86     log('\n')
87     
88     log('Other available commands:\n')
89     cmds = set()
90     for c in sorted(os.listdir(cmdpath)):
91         if c.startswith(b'bup-') and c.find(b'.') < 0:
92             cname = fsdecode(c[4:])
93             if cname not in common:
94                 cmds.add(c[4:].decode(errors='backslashreplace'))
95     # built-in commands take precedence
96     for _, name, _ in iter_modules(path=bup.cmd.__path__):
97         name = name.replace('_','-')
98         if name not in common:
99             cmds.add(name)
100
101     log(columnate(sorted(cmds), '    '))
102     log('\n')
103     
104     log("See 'bup help COMMAND' for more information on " +
105         "a specific command.\n")
106     if msg:
107         log("\n%s\n" % msg)
108     sys.exit(99)
109
110 args = compat.get_argvb()
111 if len(args) < 2:
112     usage()
113
114 ## Parse global options
115 help_requested = None
116 do_profile = False
117 bup_dir = None
118 args = args[1:]
119 while args:
120     arg = args[0]
121     if arg in (b'-?', b'--help'):
122         help_requested = True
123         args = args[1:]
124     elif arg in (b'-V', b'--version'):
125         subcmd = [b'version']
126         args = args[1:]
127     elif arg in (b'-D', b'--debug'):
128         helpers.buglvl += 1
129         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
130         args = args[1:]
131     elif arg == b'--profile':
132         do_profile = True
133         args = args[1:]
134     elif arg in (b'-d', b'--bup-dir'):
135         if len(args) < 2:
136             usage('error: no path provided for %s option' % arg)
137         bup_dir = args[1]
138         args = args[2:]
139     elif arg == b'--import-py-module':
140         args = args[2:]
141     elif arg.startswith(b'-'):
142         usage('error: unexpected option "%s"'
143               % arg.decode('ascii', 'backslashescape'))
144     else:
145         break
146
147 subcmd = args
148
149 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
150 if bup_dir:
151     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
152
153 if len(subcmd) == 0:
154     if help_requested:
155         subcmd = [b'help']
156     else:
157         usage()
158
159 if help_requested and subcmd[0] != b'help':
160     subcmd = [b'help'] + subcmd
161
162 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
163     subcmd = [b'help', subcmd[0]] + subcmd[2:]
164
165 subcmd_name = subcmd[0]
166 if not subcmd_name:
167     usage()
168
169 try:
170     cmd_module = import_module('bup.cmd.'
171                                + subcmd_name.decode('ascii').replace('-', '_'))
172 except ModuleNotFoundError as ex:
173     cmd_module = None
174
175 if not cmd_module:
176     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
177     if not os.path.exists(subcmd[0]):
178         usage('error: unknown command "%s"' % path_msg(subcmd_name))
179
180 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
181 if subcmd_name in [b'mux', b'ftp', b'help']:
182     already_fixed = True
183 fix_stdout = not already_fixed and os.isatty(1)
184 fix_stderr = not already_fixed and os.isatty(2)
185
186 if fix_stdout or fix_stderr:
187     tty_env = merge_dict(environ,
188                          {b'BUP_FORCE_TTY': (b'%d'
189                                              % ((fix_stdout and 1 or 0)
190                                                 + (fix_stderr and 2 or 0))),
191                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
192 else:
193     tty_env = environ
194
195
196 sep_rx = re.compile(br'([\r\n])')
197
198 def print_clean_line(dest, content, width, sep=None):
199     """Write some or all of content, followed by sep, to the dest fd after
200     padding the content with enough spaces to fill the current
201     terminal width or truncating it to the terminal width if sep is a
202     carriage return."""
203     global sep_rx
204     assert sep in (b'\r', b'\n', None)
205     if not content:
206         if sep:
207             os.write(dest, sep)
208         return
209     for x in content:
210         assert not sep_rx.match(x)
211     content = b''.join(content)
212     if sep == b'\r' and len(content) > width:
213         content = content[width:]
214     os.write(dest, content)
215     if len(content) < width:
216         os.write(dest, b' ' * (width - len(content)))
217     if sep:
218         os.write(dest, sep)
219
220 def filter_output(srcs, dests):
221     """Transfer data from file descriptors in srcs to the corresponding
222     file descriptors in dests print_clean_line until all of the srcs
223     have closed.
224
225     """
226     global sep_rx
227     assert all(type(x) in int_types for x in srcs)
228     assert all(type(x) in int_types for x in srcs)
229     assert len(srcs) == len(dests)
230     srcs = tuple(srcs)
231     dest_for = dict(zip(srcs, dests))
232     pending = {}
233     pending_ex = None
234     try:
235         while srcs:
236             ready_fds, _, _ = select.select(srcs, [], [])
237             width = tty_width()
238             for fd in ready_fds:
239                 buf = os.read(fd, 4096)
240                 dest = dest_for[fd]
241                 if not buf:
242                     srcs = tuple([x for x in srcs if x is not fd])
243                     print_clean_line(dest, pending.pop(fd, []), width)
244                 else:
245                     split = sep_rx.split(buf)
246                     while len(split) > 1:
247                         content, sep = split[:2]
248                         split = split[2:]
249                         print_clean_line(dest,
250                                          pending.pop(fd, []) + [content],
251                                          width,
252                                          sep)
253                     assert len(split) == 1
254                     if split[0]:
255                         pending.setdefault(fd, []).extend(split)
256     except BaseException as ex:
257         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
258     try:
259         # Try to finish each of the streams
260         for fd, pending_items in compat.items(pending):
261             dest = dest_for[fd]
262             width = tty_width()
263             try:
264                 print_clean_line(dest, pending_items, width)
265             except (EnvironmentError, EOFError) as ex:
266                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
267     except BaseException as ex:
268         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
269     if pending_ex:
270         raise pending_ex
271
272
273 def import_and_run_main(module, args):
274     if do_profile:
275         import cProfile
276         f = compile('module.main(args)', __file__, 'exec')
277         cProfile.runctx(f, globals(), locals())
278     else:
279         module.main(args)
280
281
282 def run_module_cmd(module, args):
283     if not (fix_stdout or fix_stderr):
284         import_and_run_main(module, args)
285         return
286     # Interpose filter_output between all attempts to write to the
287     # stdout/stderr and the real stdout/stderr (e.g. the fds that
288     # connect directly to the terminal) via a thread that runs
289     # filter_output in a pipeline.
290     srcs = []
291     dests = []
292     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
293     filter_thread = filter_thread_started = None
294     pending_ex = None
295     try:
296         if fix_stdout:
297             sys.stdout.flush()
298             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
299             real_out_fd = os.dup(sys.stdout.fileno())
300             os.dup2(stdout_pipe[1], sys.stdout.fileno())
301             srcs.append(stdout_pipe[0])
302             dests.append(real_out_fd)
303         if fix_stderr:
304             sys.stderr.flush()
305             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
306             real_err_fd = os.dup(sys.stderr.fileno())
307             os.dup2(stderr_pipe[1], sys.stderr.fileno())
308             srcs.append(stderr_pipe[0])
309             dests.append(real_err_fd)
310
311         filter_thread = Thread(name='output filter',
312                                target=lambda : filter_output(srcs, dests))
313         filter_thread.start()
314         filter_thread_started = True
315         import_and_run_main(module, args)
316     except Exception as ex:
317         add_ex_tb(ex)
318         pending_ex = ex
319         raise
320     finally:
321         # Try to make sure that whatever else happens, we restore
322         # stdout and stderr here, if that's possible, so that we don't
323         # risk just losing some output.
324         try:
325             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
326         except Exception as ex:
327             add_ex_tb(ex)
328             add_ex_ctx(ex, pending_ex)
329         try:
330             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
331         except Exception as ex:
332             add_ex_tb(ex)
333             add_ex_ctx(ex, pending_ex)
334         # Kick filter loose
335         try:
336             stdout_pipe is not None and os.close(stdout_pipe[1])
337         except Exception as ex:
338             add_ex_tb(ex)
339             add_ex_ctx(ex, pending_ex)
340         try:
341             stderr_pipe is not None and os.close(stderr_pipe[1])
342         except Exception as ex:
343             add_ex_tb(ex)
344             add_ex_ctx(ex, pending_ex)
345         try:
346             close_catpipes()
347         except Exception as ex:
348             add_ex_tb(ex)
349             add_ex_ctx(ex, pending_ex)
350     if pending_ex:
351         raise pending_ex
352     # There's no point in trying to join unless we finished the finally block.
353     if filter_thread_started:
354         filter_thread.join()
355
356
357 def run_subproc_cmd(args):
358
359     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
360     if not (fix_stdout or fix_stderr):
361         os.execvp(c[0], c)
362
363     sys.stdout.flush()
364     sys.stderr.flush()
365     out = byte_stream(sys.stdout)
366     err = byte_stream(sys.stderr)
367     p = None
368     try:
369         p = subprocess.Popen(c,
370                              stdout=PIPE if fix_stdout else out,
371                              stderr=PIPE if fix_stderr else err,
372                              env=tty_env, bufsize=4096, close_fds=True)
373         # Assume p will receive these signals and quit, which will
374         # then cause us to quit.
375         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
376             signal.signal(sig, signal.SIG_IGN)
377
378         srcs = []
379         dests = []
380         if fix_stdout:
381             srcs.append(p.stdout.fileno())
382             dests.append(out.fileno())
383         if fix_stderr:
384             srcs.append(p.stderr.fileno())
385             dests.append(err.fileno())
386         filter_output(srcs, dests)
387         return p.wait()
388     except BaseException as ex:
389         add_ex_tb(ex)
390         try:
391             if p and p.poll() == None:
392                 os.kill(p.pid, signal.SIGTERM)
393                 p.wait()
394         except BaseException as kill_ex:
395             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
396         raise ex
397
398
399 def run_subcmd(module, args):
400     if module:
401         run_module_cmd(module, args)
402     else:
403         run_subproc_cmd(args)
404
405 def main():
406     wrap_main(lambda : run_subcmd(cmd_module, subcmd))
407
408 if __name__ == "__main__":
409     main()