2 from __future__ import absolute_import, print_function
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
9 os.environb[b'PYTHONPATH'] = bup_main.env_pythonpath
11 del os.environ['PYTHONPATH']
13 from importlib import import_module
14 from pkgutil import iter_modules
15 from subprocess import PIPE
16 from threading import Thread
17 import re, select, signal, subprocess
19 from bup import compat, path, helpers
20 from bup.compat import (
29 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
30 from bup.helpers import (
37 from bup.git import close_catpipes
38 from bup.io import byte_stream, path_msg
39 from bup.options import _tty_width
42 def maybe_import_early(argv):
43 """Scan argv and import any modules specified by --import-py-module."""
45 if argv[0] != '--import-py-module':
49 log("bup: --import-py-module must have an argument\n")
55 maybe_import_early(compat.get_argv())
59 cmdpath = path.cmddir()
61 # We manipulate the subcmds here as strings, but they must be ASCII
62 # compatible, since we're going to be looking for exactly
63 # b'bup-SUBCMD' to exec.
66 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
67 '<command> [options...]\n\n')
69 ftp = 'Browse backup sets using an ftp-like client',
70 fsck = 'Check backup sets for damage and add redundancy information',
71 fuse = 'Mount your backup sets as a filesystem',
72 help = 'Print detailed help for the given command',
73 index = 'Create or display the index of files to back up',
74 on = 'Backup a remote machine to the local one',
75 restore = 'Extract files from a backup set',
76 save = 'Save files into a backup set (note: run "bup index" first)',
77 tag = 'Tag commits for easier access',
78 web = 'Launch a web server to examine backup sets',
81 log('Common commands:\n')
82 for cmd,synopsis in sorted(common.items()):
83 log(' %-10s %s\n' % (cmd, synopsis))
86 log('Other available commands:\n')
88 for c in sorted(os.listdir(cmdpath)):
89 if c.startswith(b'bup-') and c.find(b'.') < 0:
90 cname = fsdecode(c[4:])
91 if cname not in common:
92 cmds.add(c[4:].decode(errors='backslashreplace'))
93 # built-in commands take precedence
94 for _, name, _ in iter_modules(path=bup.cmd.__path__):
95 name = name.replace('_','-')
96 if name not in common:
99 log(columnate(sorted(cmds), ' '))
102 log("See 'bup help COMMAND' for more information on " +
103 "a specific command.\n")
108 def extract_argval(args):
109 """Assume args (all elements bytes) starts with a -x, --x, or --x=,
110 argument that requires a value and return that value and the remaining
111 args. Exit with an errror if the value is missing.
114 # Assumes that first arg is a valid arg
117 val = arg.split(b'=')[1]
119 usage('error: no value provided for %s option' % arg)
122 usage('error: no value provided for %s option' % arg)
123 return args[1], args[2:]
126 args = compat.get_argvb()
130 ## Parse global options
131 help_requested = None
137 if arg in (b'-?', b'--help'):
138 help_requested = True
140 elif arg in (b'-V', b'--version'):
141 subcmd = [b'version']
143 elif arg in (b'-D', b'--debug'):
145 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
147 elif arg == b'--profile':
150 elif arg in (b'-d', b'--bup-dir') or arg.startswith(b'--bup-dir='):
151 bup_dir, args = extract_argval(args)
152 elif arg == b'--import-py-module' or arg.startswith(b'--import-py-module='):
153 # Just need to skip it here
154 _, args = extract_argval(args)
155 elif arg.startswith(b'-'):
156 usage('error: unexpected option "%s"'
157 % arg.decode('ascii', 'backslashescape'))
163 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
165 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
173 if help_requested and subcmd[0] != b'help':
174 subcmd = [b'help'] + subcmd
176 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
177 subcmd = [b'help', subcmd[0]] + subcmd[2:]
179 subcmd_name = subcmd[0]
184 cmd_module = import_module('bup.cmd.'
185 + subcmd_name.decode('ascii').replace('-', '_'))
186 except ModuleNotFoundError as ex:
190 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
191 if not os.path.exists(subcmd[0]):
192 usage('error: unknown command "%s"' % path_msg(subcmd_name))
194 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
195 if subcmd_name in [b'mux', b'ftp', b'help']:
197 fix_stdout = not already_fixed and os.isatty(1)
198 fix_stderr = not already_fixed and os.isatty(2)
200 if fix_stdout or fix_stderr:
201 tty_env = merge_dict(environ,
202 {b'BUP_FORCE_TTY': (b'%d'
203 % ((fix_stdout and 1 or 0)
204 + (fix_stderr and 2 or 0))),
205 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
210 sep_rx = re.compile(br'([\r\n])')
212 def print_clean_line(dest, content, width, sep=None):
213 """Write some or all of content, followed by sep, to the dest fd after
214 padding the content with enough spaces to fill the current
215 terminal width or truncating it to the terminal width if sep is a
218 assert sep in (b'\r', b'\n', None)
224 assert not sep_rx.match(x)
225 content = b''.join(content)
226 if sep == b'\r' and len(content) > width:
227 content = content[width:]
228 os.write(dest, content)
229 if len(content) < width:
230 os.write(dest, b' ' * (width - len(content)))
234 def filter_output(srcs, dests):
235 """Transfer data from file descriptors in srcs to the corresponding
236 file descriptors in dests print_clean_line until all of the srcs
241 assert all(type(x) in int_types for x in srcs)
242 assert all(type(x) in int_types for x in srcs)
243 assert len(srcs) == len(dests)
245 dest_for = dict(zip(srcs, dests))
250 ready_fds, _, _ = select.select(srcs, [], [])
253 buf = os.read(fd, 4096)
256 srcs = tuple([x for x in srcs if x is not fd])
257 print_clean_line(dest, pending.pop(fd, []), width)
259 split = sep_rx.split(buf)
260 while len(split) > 1:
261 content, sep = split[:2]
263 print_clean_line(dest,
264 pending.pop(fd, []) + [content],
267 assert len(split) == 1
269 pending.setdefault(fd, []).extend(split)
270 except BaseException as ex:
271 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
273 # Try to finish each of the streams
274 for fd, pending_items in compat.items(pending):
278 print_clean_line(dest, pending_items, width)
279 except (EnvironmentError, EOFError) as ex:
280 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
281 except BaseException as ex:
282 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
287 def import_and_run_main(module, args):
290 f = compile('module.main(args)', __file__, 'exec')
291 cProfile.runctx(f, globals(), locals())
296 def run_module_cmd(module, args):
297 if not (fix_stdout or fix_stderr):
298 import_and_run_main(module, args)
300 # Interpose filter_output between all attempts to write to the
301 # stdout/stderr and the real stdout/stderr (e.g. the fds that
302 # connect directly to the terminal) via a thread that runs
303 # filter_output in a pipeline.
306 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
307 filter_thread = filter_thread_started = None
312 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
313 real_out_fd = os.dup(sys.stdout.fileno())
314 os.dup2(stdout_pipe[1], sys.stdout.fileno())
315 srcs.append(stdout_pipe[0])
316 dests.append(real_out_fd)
319 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
320 real_err_fd = os.dup(sys.stderr.fileno())
321 os.dup2(stderr_pipe[1], sys.stderr.fileno())
322 srcs.append(stderr_pipe[0])
323 dests.append(real_err_fd)
325 filter_thread = Thread(name='output filter',
326 target=lambda : filter_output(srcs, dests))
327 filter_thread.start()
328 filter_thread_started = True
329 import_and_run_main(module, args)
330 except Exception as ex:
335 # Try to make sure that whatever else happens, we restore
336 # stdout and stderr here, if that's possible, so that we don't
337 # risk just losing some output.
339 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
340 except Exception as ex:
342 add_ex_ctx(ex, pending_ex)
344 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
345 except Exception as ex:
347 add_ex_ctx(ex, pending_ex)
350 stdout_pipe is not None and os.close(stdout_pipe[1])
351 except Exception as ex:
353 add_ex_ctx(ex, pending_ex)
355 stderr_pipe is not None and os.close(stderr_pipe[1])
356 except Exception as ex:
358 add_ex_ctx(ex, pending_ex)
361 except Exception as ex:
363 add_ex_ctx(ex, pending_ex)
366 # There's no point in trying to join unless we finished the finally block.
367 if filter_thread_started:
371 def run_subproc_cmd(args):
373 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
374 if not (fix_stdout or fix_stderr):
379 out = byte_stream(sys.stdout)
380 err = byte_stream(sys.stderr)
383 p = subprocess.Popen(c,
384 stdout=PIPE if fix_stdout else out,
385 stderr=PIPE if fix_stderr else err,
386 env=tty_env, bufsize=4096, close_fds=True)
387 # Assume p will receive these signals and quit, which will
388 # then cause us to quit.
389 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
390 signal.signal(sig, signal.SIG_IGN)
395 srcs.append(p.stdout.fileno())
396 dests.append(out.fileno())
398 srcs.append(p.stderr.fileno())
399 dests.append(err.fileno())
400 filter_output(srcs, dests)
402 except BaseException as ex:
405 if p and p.poll() == None:
406 os.kill(p.pid, signal.SIGTERM)
408 except BaseException as kill_ex:
409 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
413 def run_subcmd(module, args):
415 run_module_cmd(module, args)
417 run_subproc_cmd(args)
420 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
422 if __name__ == "__main__":