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 (
28 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
29 from bup.helpers import (
35 from bup.git import close_catpipes
36 from bup.io import byte_stream, path_msg
37 from bup.options import _tty_width
40 def maybe_import_early(argv):
41 """Scan argv and import any modules specified by --import-py-module."""
43 if argv[0] != '--import-py-module':
47 log("bup: --import-py-module must have an argument\n")
53 maybe_import_early(compat.get_argv())
57 cmdpath = path.cmddir()
59 # We manipulate the subcmds here as strings, but they must be ASCII
60 # compatible, since we're going to be looking for exactly
61 # b'bup-SUBCMD' to exec.
64 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
65 '<command> [options...]\n\n')
67 ftp = 'Browse backup sets using an ftp-like client',
68 fsck = 'Check backup sets for damage and add redundancy information',
69 fuse = 'Mount your backup sets as a filesystem',
70 help = 'Print detailed help for the given command',
71 index = 'Create or display the index of files to back up',
72 on = 'Backup a remote machine to the local one',
73 restore = 'Extract files from a backup set',
74 save = 'Save files into a backup set (note: run "bup index" first)',
75 tag = 'Tag commits for easier access',
76 web = 'Launch a web server to examine backup sets',
79 log('Common commands:\n')
80 for cmd,synopsis in sorted(common.items()):
81 log(' %-10s %s\n' % (cmd, synopsis))
84 log('Other available commands:\n')
86 for c in sorted(os.listdir(cmdpath)):
87 if c.startswith(b'bup-') and c.find(b'.') < 0:
88 cname = fsdecode(c[4:])
89 if cname not in common:
90 cmds.add(c[4:].decode(errors='backslashreplace'))
91 # built-in commands take precedence
92 for _, name, _ in iter_modules(path=bup.cmd.__path__):
93 name = name.replace('_','-')
94 if name not in common:
97 log(columnate(sorted(cmds), ' '))
100 log("See 'bup help COMMAND' for more information on " +
101 "a specific command.\n")
106 def extract_argval(args):
107 """Assume args (all elements bytes) starts with a -x, --x, or --x=,
108 argument that requires a value and return that value and the remaining
109 args. Exit with an errror if the value is missing.
112 # Assumes that first arg is a valid arg
115 val = arg.split(b'=')[1]
117 usage('error: no value provided for %s option' % arg)
120 usage('error: no value provided for %s option' % arg)
121 return args[1], args[2:]
124 args = compat.get_argvb()
128 ## Parse global options
129 help_requested = None
135 if arg in (b'-?', b'--help'):
136 help_requested = True
138 elif arg in (b'-V', b'--version'):
139 subcmd = [b'version']
141 elif arg in (b'-D', b'--debug'):
143 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
145 elif arg == b'--profile':
148 elif arg in (b'-d', b'--bup-dir') or arg.startswith(b'--bup-dir='):
149 bup_dir, args = extract_argval(args)
150 elif arg == b'--import-py-module' or arg.startswith(b'--import-py-module='):
151 # Just need to skip it here
152 _, args = extract_argval(args)
153 elif arg.startswith(b'-'):
154 usage('error: unexpected option "%s"'
155 % arg.decode('ascii', 'backslashescape'))
161 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
163 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
171 if help_requested and subcmd[0] != b'help':
172 subcmd = [b'help'] + subcmd
174 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
175 subcmd = [b'help', subcmd[0]] + subcmd[2:]
177 subcmd_name = subcmd[0]
182 cmd_module = import_module('bup.cmd.'
183 + subcmd_name.decode('ascii').replace('-', '_'))
184 except ModuleNotFoundError as ex:
188 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
189 if not os.path.exists(subcmd[0]):
190 usage('error: unknown command "%s"' % path_msg(subcmd_name))
192 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
193 if subcmd_name in [b'mux', b'ftp', b'help']:
197 fix_stdout = not (already_fixed & 1) and os.isatty(1)
198 fix_stderr = not (already_fixed & 2) and os.isatty(2)
200 if fix_stdout or fix_stderr:
201 _ttymask = (fix_stdout and 1 or 0) + (fix_stderr and 2 or 0)
202 environ[b'BUP_FORCE_TTY'] = b'%d' % _ttymask
203 environ[b'BUP_TTY_WIDTH'] = b'%d' % _tty_width()
206 sep_rx = re.compile(br'([\r\n])')
208 def print_clean_line(dest, content, width, sep=None):
209 """Write some or all of content, followed by sep, to the dest fd after
210 padding the content with enough spaces to fill the current
211 terminal width or truncating it to the terminal width if sep is a
214 assert sep in (b'\r', b'\n', None)
220 assert not sep_rx.match(x)
221 content = b''.join(content)
222 if sep == b'\r' and len(content) > width:
223 content = content[:width]
224 os.write(dest, content)
225 if len(content) < width:
226 os.write(dest, b' ' * (width - len(content)))
230 def filter_output(srcs, dests):
231 """Transfer data from file descriptors in srcs to the corresponding
232 file descriptors in dests print_clean_line until all of the srcs
237 assert all(isinstance(x, int_types) for x in srcs)
238 assert len(srcs) == len(dests)
240 dest_for = dict(zip(srcs, dests))
245 ready_fds, _, _ = select.select(srcs, [], [])
248 buf = os.read(fd, 4096)
251 srcs = tuple([x for x in srcs if x is not fd])
252 print_clean_line(dest, pending.pop(fd, []), width)
254 split = sep_rx.split(buf)
255 while len(split) > 1:
256 content, sep = split[:2]
258 print_clean_line(dest,
259 pending.pop(fd, []) + [content],
262 assert len(split) == 1
264 pending.setdefault(fd, []).extend(split)
265 except BaseException as ex:
266 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
268 # Try to finish each of the streams
269 for fd, pending_items in compat.items(pending):
273 print_clean_line(dest, pending_items, width)
274 except (EnvironmentError, EOFError) as ex:
275 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
276 except BaseException as ex:
277 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
282 def import_and_run_main(module, args):
285 f = compile('module.main(args)', __file__, 'exec')
286 cProfile.runctx(f, globals(), locals())
291 def run_module_cmd(module, args):
292 if not (fix_stdout or fix_stderr):
293 import_and_run_main(module, args)
295 # Interpose filter_output between all attempts to write to the
296 # stdout/stderr and the real stdout/stderr (e.g. the fds that
297 # connect directly to the terminal) via a thread that runs
298 # filter_output in a pipeline.
301 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
302 filter_thread = filter_thread_started = None
307 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
308 real_out_fd = os.dup(sys.stdout.fileno())
309 os.dup2(stdout_pipe[1], sys.stdout.fileno())
310 srcs.append(stdout_pipe[0])
311 dests.append(real_out_fd)
314 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
315 real_err_fd = os.dup(sys.stderr.fileno())
316 os.dup2(stderr_pipe[1], sys.stderr.fileno())
317 srcs.append(stderr_pipe[0])
318 dests.append(real_err_fd)
320 filter_thread = Thread(name='output filter',
321 target=lambda : filter_output(srcs, dests))
322 filter_thread.start()
323 filter_thread_started = True
324 import_and_run_main(module, args)
325 except Exception as ex:
330 # Try to make sure that whatever else happens, we restore
331 # stdout and stderr here, if that's possible, so that we don't
332 # risk just losing some output.
334 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
335 except Exception as ex:
337 add_ex_ctx(ex, pending_ex)
339 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
340 except Exception as ex:
342 add_ex_ctx(ex, pending_ex)
345 stdout_pipe is not None and os.close(stdout_pipe[1])
346 except Exception as ex:
348 add_ex_ctx(ex, pending_ex)
350 stderr_pipe is not None and os.close(stderr_pipe[1])
351 except Exception as ex:
353 add_ex_ctx(ex, pending_ex)
356 except Exception as ex:
358 add_ex_ctx(ex, pending_ex)
361 # There's no point in trying to join unless we finished the finally block.
362 if filter_thread_started:
366 def run_subproc_cmd(args):
368 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
369 if not (fix_stdout or fix_stderr):
374 out = byte_stream(sys.stdout)
375 err = byte_stream(sys.stderr)
378 p = subprocess.Popen(c,
379 stdout=PIPE if fix_stdout else out,
380 stderr=PIPE if fix_stderr else err,
381 bufsize=4096, close_fds=True)
382 # Assume p will receive these signals and quit, which will
383 # then cause us to quit.
384 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
385 signal.signal(sig, signal.SIG_IGN)
390 srcs.append(p.stdout.fileno())
391 dests.append(out.fileno())
393 srcs.append(p.stderr.fileno())
394 dests.append(err.fileno())
395 filter_output(srcs, dests)
397 except BaseException as ex:
400 if p and p.poll() == None:
401 os.kill(p.pid, signal.SIGTERM)
403 except BaseException as kill_ex:
404 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
408 def run_subcmd(module, args):
410 run_module_cmd(module, args)
412 run_subproc_cmd(args)
415 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
417 if __name__ == "__main__":