2 from __future__ import absolute_import, print_function
3 from importlib import import_module
4 from pkgutil import iter_modules
5 from subprocess import PIPE
6 from threading import Thread
7 import errno, getopt, os, re, select, signal, subprocess, sys
9 from bup import compat, path, helpers
10 from bup.compat import (
20 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
21 from bup.helpers import (
29 from bup.git import close_catpipes
30 from bup.io import byte_stream, path_msg
31 from bup.options import _tty_width
34 def maybe_import_early(argv):
35 """Scan argv and import any modules specified by --import-py-module."""
37 if argv[0] != '--import-py-module':
41 log("bup: --import-py-module must have an argument\n")
47 maybe_import_early(compat.get_argv())
51 cmdpath = path.cmddir()
53 # Remove once we finish the internal command transition
54 transition_cmdpath = path.libdir() + b'/bup/cmd'
56 # We manipulate the subcmds here as strings, but they must be ASCII
57 # compatible, since we're going to be looking for exactly
58 # b'bup-SUBCMD' to exec.
61 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
62 '<command> [options...]\n\n')
64 ftp = 'Browse backup sets using an ftp-like client',
65 fsck = 'Check backup sets for damage and add redundancy information',
66 fuse = 'Mount your backup sets as a filesystem',
67 help = 'Print detailed help for the given command',
68 index = 'Create or display the index of files to back up',
69 on = 'Backup a remote machine to the local one',
70 restore = 'Extract files from a backup set',
71 save = 'Save files into a backup set (note: run "bup index" first)',
72 tag = 'Tag commits for easier access',
73 web = 'Launch a web server to examine backup sets',
76 log('Common commands:\n')
77 for cmd,synopsis in sorted(common.items()):
78 log(' %-10s %s\n' % (cmd, synopsis))
81 log('Other available commands:\n')
83 for c in sorted(os.listdir(cmdpath)):
84 if c.startswith(b'bup-') and c.find(b'.') < 0:
85 cname = fsdecode(c[4:])
86 if cname not in common:
87 cmds.add(c[4:].decode(errors='backslashreplace'))
88 # built-in commands take precedence
89 for _, name, _ in iter_modules(path=bup.cmd.__path__):
90 name = name.replace('_','-')
91 if name not in common:
94 log(columnate(sorted(cmds), ' '))
97 log("See 'bup help COMMAND' for more information on " +
98 "a specific command.\n")
103 argv = compat.get_argv()
107 # Handle global options.
109 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
111 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
112 except getopt.GetoptError as ex:
113 usage('error: %s' % ex.msg)
115 subcmd = [argv_bytes(x) for x in subcmd]
116 help_requested = None
120 for opt in global_args:
121 if opt[0] in ['-?', '--help']:
122 help_requested = True
123 elif opt[0] in ['-V', '--version']:
124 subcmd = [b'version']
125 elif opt[0] in ['-D', '--debug']:
127 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
128 elif opt[0] in ['--profile']:
130 elif opt[0] in ['-d', '--bup-dir']:
131 bup_dir = argv_bytes(opt[1])
132 elif opt[0] == '--import-py-module':
135 usage('error: unexpected option "%s"' % opt[0])
137 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
139 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
147 if help_requested and subcmd[0] != b'help':
148 subcmd = [b'help'] + subcmd
150 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
151 subcmd = [b'help', subcmd[0]] + subcmd[2:]
153 subcmd_name = subcmd[0]
158 if subcmd_name not in (b'bloom',
195 raise ModuleNotFoundError()
196 cmd_module = import_module('bup.cmd.'
197 + subcmd_name.decode('ascii').replace('-', '_'))
198 except ModuleNotFoundError as ex:
202 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
203 if not os.path.exists(subcmd[0]):
204 subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
205 subcmd_name.replace(b'-', b'_'))
206 if not os.path.exists(subcmd[0]):
207 usage('error: unknown command "%s"' % path_msg(subcmd_name))
209 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
210 if subcmd_name in [b'mux', b'ftp', b'help']:
212 fix_stdout = not already_fixed and os.isatty(1)
213 fix_stderr = not already_fixed and os.isatty(2)
215 if fix_stdout or fix_stderr:
216 tty_env = merge_dict(environ,
217 {b'BUP_FORCE_TTY': (b'%d'
218 % ((fix_stdout and 1 or 0)
219 + (fix_stderr and 2 or 0))),
220 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
225 sep_rx = re.compile(br'([\r\n])')
227 def print_clean_line(dest, content, width, sep=None):
228 """Write some or all of content, followed by sep, to the dest fd after
229 padding the content with enough spaces to fill the current
230 terminal width or truncating it to the terminal width if sep is a
233 assert sep in (b'\r', b'\n', None)
239 assert not sep_rx.match(x)
240 content = b''.join(content)
241 if sep == b'\r' and len(content) > width:
242 content = content[width:]
243 os.write(dest, content)
244 if len(content) < width:
245 os.write(dest, b' ' * (width - len(content)))
249 def filter_output(srcs, dests):
250 """Transfer data from file descriptors in srcs to the corresponding
251 file descriptors in dests print_clean_line until all of the srcs
256 assert all(type(x) in int_types for x in srcs)
257 assert all(type(x) in int_types for x in srcs)
258 assert len(srcs) == len(dests)
260 dest_for = dict(zip(srcs, dests))
265 ready_fds, _, _ = select.select(srcs, [], [])
268 buf = os.read(fd, 4096)
271 srcs = tuple([x for x in srcs if x is not fd])
272 print_clean_line(dest, pending.pop(fd, []), width)
274 split = sep_rx.split(buf)
275 while len(split) > 1:
276 content, sep = split[:2]
278 print_clean_line(dest,
279 pending.pop(fd, []) + [content],
282 assert len(split) == 1
284 pending.setdefault(fd, []).extend(split)
285 except BaseException as ex:
286 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
288 # Try to finish each of the streams
289 for fd, pending_items in compat.items(pending):
293 print_clean_line(dest, pending_items, width)
294 except (EnvironmentError, EOFError) as ex:
295 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
296 except BaseException as ex:
297 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
302 def import_and_run_main(module, args):
305 f = compile('module.main(args)', __file__, 'exec')
306 cProfile.runctx(f, globals(), locals())
311 def run_module_cmd(module, args):
312 if not (fix_stdout or fix_stderr):
313 import_and_run_main(module, args)
315 # Interpose filter_output between all attempts to write to the
316 # stdout/stderr and the real stdout/stderr (e.g. the fds that
317 # connect directly to the terminal) via a thread that runs
318 # filter_output in a pipeline.
321 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
322 filter_thread = filter_thread_started = None
327 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
328 real_out_fd = os.dup(sys.stdout.fileno())
329 os.dup2(stdout_pipe[1], sys.stdout.fileno())
330 srcs.append(stdout_pipe[0])
331 dests.append(real_out_fd)
334 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
335 real_err_fd = os.dup(sys.stderr.fileno())
336 os.dup2(stderr_pipe[1], sys.stderr.fileno())
337 srcs.append(stderr_pipe[0])
338 dests.append(real_err_fd)
340 filter_thread = Thread(name='output filter',
341 target=lambda : filter_output(srcs, dests))
342 filter_thread.start()
343 filter_thread_started = True
344 import_and_run_main(module, args)
345 except Exception as ex:
350 # Try to make sure that whatever else happens, we restore
351 # stdout and stderr here, if that's possible, so that we don't
352 # risk just losing some output.
354 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
355 except Exception as ex:
357 add_ex_ctx(ex, pending_ex)
359 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
360 except Exception as ex:
362 add_ex_ctx(ex, pending_ex)
365 stdout_pipe is not None and os.close(stdout_pipe[1])
366 except Exception as ex:
368 add_ex_ctx(ex, pending_ex)
370 stderr_pipe is not None and os.close(stderr_pipe[1])
371 except Exception as ex:
373 add_ex_ctx(ex, pending_ex)
376 except Exception as ex:
378 add_ex_ctx(ex, pending_ex)
381 # There's no point in trying to join unless we finished the finally block.
382 if filter_thread_started:
386 def run_subproc_cmd(args):
388 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
389 if not (fix_stdout or fix_stderr):
394 out = byte_stream(sys.stdout)
395 err = byte_stream(sys.stderr)
398 p = subprocess.Popen(c,
399 stdout=PIPE if fix_stdout else out,
400 stderr=PIPE if fix_stderr else err,
401 env=tty_env, bufsize=4096, close_fds=True)
402 # Assume p will receive these signals and quit, which will
403 # then cause us to quit.
404 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
405 signal.signal(sig, signal.SIG_IGN)
410 srcs.append(p.stdout.fileno())
411 dests.append(out.fileno())
413 srcs.append(p.stderr.fileno())
414 dests.append(err.fileno())
415 filter_output(srcs, dests)
417 except BaseException as ex:
420 if p and p.poll() == None:
421 os.kill(p.pid, signal.SIGTERM)
423 except BaseException as kill_ex:
424 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
428 def run_subcmd(module, args):
430 run_module_cmd(module, args)
432 run_subproc_cmd(args)
435 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
437 if __name__ == "__main__":