]> arthur.barton.de Git - bup.git/blob - lib/bup/main.py
e24bee9b9f45194624aeca345109bb0eaf6fb9c8
[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 def extract_argval(args):
111     """Assume args (all elements bytes) starts with a -x, --x, or --x=,
112 argument that requires a value and return that value and the remaining
113 args.  Exit with an errror if the value is missing.
114
115     """
116     # Assumes that first arg is a valid arg
117     arg = args[0]
118     if b'=' in arg:
119         val = arg.split(b'=')[1]
120         if not val:
121             usage('error: no value provided for %s option' % arg)
122         return val, args[1:]
123     if len(args) < 2:
124         usage('error: no value provided for %s option' % arg)
125     return args[1], args[2:]
126
127
128 args = compat.get_argvb()
129 if len(args) < 2:
130     usage()
131
132 ## Parse global options
133 help_requested = None
134 do_profile = False
135 bup_dir = None
136 args = args[1:]
137 while args:
138     arg = args[0]
139     if arg in (b'-?', b'--help'):
140         help_requested = True
141         args = args[1:]
142     elif arg in (b'-V', b'--version'):
143         subcmd = [b'version']
144         args = args[1:]
145     elif arg in (b'-D', b'--debug'):
146         helpers.buglvl += 1
147         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
148         args = args[1:]
149     elif arg == b'--profile':
150         do_profile = True
151         args = args[1:]
152     elif arg in (b'-d', b'--bup-dir') or arg.startswith(b'--bup-dir='):
153         bup_dir, args = extract_argval(args)
154     elif arg == b'--import-py-module' or arg.startswith(b'--import-py-module='):
155         # Just need to skip it here
156         _, args = extract_argval(args)
157     elif arg.startswith(b'-'):
158         usage('error: unexpected option "%s"'
159               % arg.decode('ascii', 'backslashescape'))
160     else:
161         break
162
163 subcmd = args
164
165 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
166 if bup_dir:
167     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
168
169 if len(subcmd) == 0:
170     if help_requested:
171         subcmd = [b'help']
172     else:
173         usage()
174
175 if help_requested and subcmd[0] != b'help':
176     subcmd = [b'help'] + subcmd
177
178 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
179     subcmd = [b'help', subcmd[0]] + subcmd[2:]
180
181 subcmd_name = subcmd[0]
182 if not subcmd_name:
183     usage()
184
185 try:
186     cmd_module = import_module('bup.cmd.'
187                                + subcmd_name.decode('ascii').replace('-', '_'))
188 except ModuleNotFoundError as ex:
189     cmd_module = None
190
191 if not cmd_module:
192     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
193     if not os.path.exists(subcmd[0]):
194         usage('error: unknown command "%s"' % path_msg(subcmd_name))
195
196 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
197 if subcmd_name in [b'mux', b'ftp', b'help']:
198     already_fixed = True
199 fix_stdout = not already_fixed and os.isatty(1)
200 fix_stderr = not already_fixed and os.isatty(2)
201
202 if fix_stdout or fix_stderr:
203     tty_env = merge_dict(environ,
204                          {b'BUP_FORCE_TTY': (b'%d'
205                                              % ((fix_stdout and 1 or 0)
206                                                 + (fix_stderr and 2 or 0))),
207                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
208 else:
209     tty_env = environ
210
211
212 sep_rx = re.compile(br'([\r\n])')
213
214 def print_clean_line(dest, content, width, sep=None):
215     """Write some or all of content, followed by sep, to the dest fd after
216     padding the content with enough spaces to fill the current
217     terminal width or truncating it to the terminal width if sep is a
218     carriage return."""
219     global sep_rx
220     assert sep in (b'\r', b'\n', None)
221     if not content:
222         if sep:
223             os.write(dest, sep)
224         return
225     for x in content:
226         assert not sep_rx.match(x)
227     content = b''.join(content)
228     if sep == b'\r' and len(content) > width:
229         content = content[width:]
230     os.write(dest, content)
231     if len(content) < width:
232         os.write(dest, b' ' * (width - len(content)))
233     if sep:
234         os.write(dest, sep)
235
236 def filter_output(srcs, dests):
237     """Transfer data from file descriptors in srcs to the corresponding
238     file descriptors in dests print_clean_line until all of the srcs
239     have closed.
240
241     """
242     global sep_rx
243     assert all(type(x) in int_types for x in srcs)
244     assert all(type(x) in int_types for x in srcs)
245     assert len(srcs) == len(dests)
246     srcs = tuple(srcs)
247     dest_for = dict(zip(srcs, dests))
248     pending = {}
249     pending_ex = None
250     try:
251         while srcs:
252             ready_fds, _, _ = select.select(srcs, [], [])
253             width = tty_width()
254             for fd in ready_fds:
255                 buf = os.read(fd, 4096)
256                 dest = dest_for[fd]
257                 if not buf:
258                     srcs = tuple([x for x in srcs if x is not fd])
259                     print_clean_line(dest, pending.pop(fd, []), width)
260                 else:
261                     split = sep_rx.split(buf)
262                     while len(split) > 1:
263                         content, sep = split[:2]
264                         split = split[2:]
265                         print_clean_line(dest,
266                                          pending.pop(fd, []) + [content],
267                                          width,
268                                          sep)
269                     assert len(split) == 1
270                     if split[0]:
271                         pending.setdefault(fd, []).extend(split)
272     except BaseException as ex:
273         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
274     try:
275         # Try to finish each of the streams
276         for fd, pending_items in compat.items(pending):
277             dest = dest_for[fd]
278             width = tty_width()
279             try:
280                 print_clean_line(dest, pending_items, width)
281             except (EnvironmentError, EOFError) as ex:
282                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
283     except BaseException as ex:
284         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
285     if pending_ex:
286         raise pending_ex
287
288
289 def import_and_run_main(module, args):
290     if do_profile:
291         import cProfile
292         f = compile('module.main(args)', __file__, 'exec')
293         cProfile.runctx(f, globals(), locals())
294     else:
295         module.main(args)
296
297
298 def run_module_cmd(module, args):
299     if not (fix_stdout or fix_stderr):
300         import_and_run_main(module, args)
301         return
302     # Interpose filter_output between all attempts to write to the
303     # stdout/stderr and the real stdout/stderr (e.g. the fds that
304     # connect directly to the terminal) via a thread that runs
305     # filter_output in a pipeline.
306     srcs = []
307     dests = []
308     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
309     filter_thread = filter_thread_started = None
310     pending_ex = None
311     try:
312         if fix_stdout:
313             sys.stdout.flush()
314             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
315             real_out_fd = os.dup(sys.stdout.fileno())
316             os.dup2(stdout_pipe[1], sys.stdout.fileno())
317             srcs.append(stdout_pipe[0])
318             dests.append(real_out_fd)
319         if fix_stderr:
320             sys.stderr.flush()
321             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
322             real_err_fd = os.dup(sys.stderr.fileno())
323             os.dup2(stderr_pipe[1], sys.stderr.fileno())
324             srcs.append(stderr_pipe[0])
325             dests.append(real_err_fd)
326
327         filter_thread = Thread(name='output filter',
328                                target=lambda : filter_output(srcs, dests))
329         filter_thread.start()
330         filter_thread_started = True
331         import_and_run_main(module, args)
332     except Exception as ex:
333         add_ex_tb(ex)
334         pending_ex = ex
335         raise
336     finally:
337         # Try to make sure that whatever else happens, we restore
338         # stdout and stderr here, if that's possible, so that we don't
339         # risk just losing some output.
340         try:
341             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
342         except Exception as ex:
343             add_ex_tb(ex)
344             add_ex_ctx(ex, pending_ex)
345         try:
346             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
347         except Exception as ex:
348             add_ex_tb(ex)
349             add_ex_ctx(ex, pending_ex)
350         # Kick filter loose
351         try:
352             stdout_pipe is not None and os.close(stdout_pipe[1])
353         except Exception as ex:
354             add_ex_tb(ex)
355             add_ex_ctx(ex, pending_ex)
356         try:
357             stderr_pipe is not None and os.close(stderr_pipe[1])
358         except Exception as ex:
359             add_ex_tb(ex)
360             add_ex_ctx(ex, pending_ex)
361         try:
362             close_catpipes()
363         except Exception as ex:
364             add_ex_tb(ex)
365             add_ex_ctx(ex, pending_ex)
366     if pending_ex:
367         raise pending_ex
368     # There's no point in trying to join unless we finished the finally block.
369     if filter_thread_started:
370         filter_thread.join()
371
372
373 def run_subproc_cmd(args):
374
375     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
376     if not (fix_stdout or fix_stderr):
377         os.execvp(c[0], c)
378
379     sys.stdout.flush()
380     sys.stderr.flush()
381     out = byte_stream(sys.stdout)
382     err = byte_stream(sys.stderr)
383     p = None
384     try:
385         p = subprocess.Popen(c,
386                              stdout=PIPE if fix_stdout else out,
387                              stderr=PIPE if fix_stderr else err,
388                              env=tty_env, bufsize=4096, close_fds=True)
389         # Assume p will receive these signals and quit, which will
390         # then cause us to quit.
391         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
392             signal.signal(sig, signal.SIG_IGN)
393
394         srcs = []
395         dests = []
396         if fix_stdout:
397             srcs.append(p.stdout.fileno())
398             dests.append(out.fileno())
399         if fix_stderr:
400             srcs.append(p.stderr.fileno())
401             dests.append(err.fileno())
402         filter_output(srcs, dests)
403         return p.wait()
404     except BaseException as ex:
405         add_ex_tb(ex)
406         try:
407             if p and p.poll() == None:
408                 os.kill(p.pid, signal.SIGTERM)
409                 p.wait()
410         except BaseException as kill_ex:
411             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
412         raise ex
413
414
415 def run_subcmd(module, args):
416     if module:
417         run_module_cmd(module, args)
418     else:
419         run_subproc_cmd(args)
420
421 def main():
422     wrap_main(lambda : run_subcmd(cmd_module, subcmd))
423
424 if __name__ == "__main__":
425     main()