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 errno, re, select, signal, subprocess
19 from bup import compat, path, helpers
20 from bup.compat import (
30 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
31 from bup.helpers import (
39 from bup.git import close_catpipes
40 from bup.io import byte_stream, path_msg
41 from bup.options import _tty_width
44 def maybe_import_early(argv):
45 """Scan argv and import any modules specified by --import-py-module."""
47 if argv[0] != '--import-py-module':
51 log("bup: --import-py-module must have an argument\n")
57 maybe_import_early(compat.get_argv())
61 cmdpath = path.cmddir()
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.
68 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
69 '<command> [options...]\n\n')
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',
83 log('Common commands:\n')
84 for cmd,synopsis in sorted(common.items()):
85 log(' %-10s %s\n' % (cmd, synopsis))
88 log('Other available commands:\n')
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:
101 log(columnate(sorted(cmds), ' '))
104 log("See 'bup help COMMAND' for more information on " +
105 "a specific command.\n")
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.
116 # Assumes that first arg is a valid arg
119 val = arg.split(b'=')[1]
121 usage('error: no value provided for %s option' % arg)
124 usage('error: no value provided for %s option' % arg)
125 return args[1], args[2:]
128 args = compat.get_argvb()
132 ## Parse global options
133 help_requested = None
139 if arg in (b'-?', b'--help'):
140 help_requested = True
142 elif arg in (b'-V', b'--version'):
143 subcmd = [b'version']
145 elif arg in (b'-D', b'--debug'):
147 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
149 elif arg == b'--profile':
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'))
165 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
167 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
175 if help_requested and subcmd[0] != b'help':
176 subcmd = [b'help'] + subcmd
178 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
179 subcmd = [b'help', subcmd[0]] + subcmd[2:]
181 subcmd_name = subcmd[0]
186 cmd_module = import_module('bup.cmd.'
187 + subcmd_name.decode('ascii').replace('-', '_'))
188 except ModuleNotFoundError as ex:
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))
196 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
197 if subcmd_name in [b'mux', b'ftp', b'help']:
199 fix_stdout = not already_fixed and os.isatty(1)
200 fix_stderr = not already_fixed and os.isatty(2)
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(), })
212 sep_rx = re.compile(br'([\r\n])')
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
220 assert sep in (b'\r', b'\n', None)
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)))
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
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)
247 dest_for = dict(zip(srcs, dests))
252 ready_fds, _, _ = select.select(srcs, [], [])
255 buf = os.read(fd, 4096)
258 srcs = tuple([x for x in srcs if x is not fd])
259 print_clean_line(dest, pending.pop(fd, []), width)
261 split = sep_rx.split(buf)
262 while len(split) > 1:
263 content, sep = split[:2]
265 print_clean_line(dest,
266 pending.pop(fd, []) + [content],
269 assert len(split) == 1
271 pending.setdefault(fd, []).extend(split)
272 except BaseException as ex:
273 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
275 # Try to finish each of the streams
276 for fd, pending_items in compat.items(pending):
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)
289 def import_and_run_main(module, args):
292 f = compile('module.main(args)', __file__, 'exec')
293 cProfile.runctx(f, globals(), locals())
298 def run_module_cmd(module, args):
299 if not (fix_stdout or fix_stderr):
300 import_and_run_main(module, args)
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.
308 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
309 filter_thread = filter_thread_started = None
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)
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)
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:
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.
341 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
342 except Exception as ex:
344 add_ex_ctx(ex, pending_ex)
346 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
347 except Exception as ex:
349 add_ex_ctx(ex, pending_ex)
352 stdout_pipe is not None and os.close(stdout_pipe[1])
353 except Exception as ex:
355 add_ex_ctx(ex, pending_ex)
357 stderr_pipe is not None and os.close(stderr_pipe[1])
358 except Exception as ex:
360 add_ex_ctx(ex, pending_ex)
363 except Exception as ex:
365 add_ex_ctx(ex, pending_ex)
368 # There's no point in trying to join unless we finished the finally block.
369 if filter_thread_started:
373 def run_subproc_cmd(args):
375 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
376 if not (fix_stdout or fix_stderr):
381 out = byte_stream(sys.stdout)
382 err = byte_stream(sys.stderr)
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)
397 srcs.append(p.stdout.fileno())
398 dests.append(out.fileno())
400 srcs.append(p.stderr.fileno())
401 dests.append(err.fileno())
402 filter_output(srcs, dests)
404 except BaseException as ex:
407 if p and p.poll() == None:
408 os.kill(p.pid, signal.SIGTERM)
410 except BaseException as kill_ex:
411 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
415 def run_subcmd(module, args):
417 run_module_cmd(module, args)
419 run_subproc_cmd(args)
422 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
424 if __name__ == "__main__":