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 (
36 from bup.git import close_catpipes
37 from bup.io import byte_stream, path_msg
38 from bup.options import _tty_width
41 def maybe_import_early(argv):
42 """Scan argv and import any modules specified by --import-py-module."""
44 if argv[0] != '--import-py-module':
48 log("bup: --import-py-module must have an argument\n")
54 maybe_import_early(compat.get_argv())
58 cmdpath = path.cmddir()
60 # We manipulate the subcmds here as strings, but they must be ASCII
61 # compatible, since we're going to be looking for exactly
62 # b'bup-SUBCMD' to exec.
65 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
66 '<command> [options...]\n\n')
68 ftp = 'Browse backup sets using an ftp-like client',
69 fsck = 'Check backup sets for damage and add redundancy information',
70 fuse = 'Mount your backup sets as a filesystem',
71 help = 'Print detailed help for the given command',
72 index = 'Create or display the index of files to back up',
73 on = 'Backup a remote machine to the local one',
74 restore = 'Extract files from a backup set',
75 save = 'Save files into a backup set (note: run "bup index" first)',
76 tag = 'Tag commits for easier access',
77 web = 'Launch a web server to examine backup sets',
80 log('Common commands:\n')
81 for cmd,synopsis in sorted(common.items()):
82 log(' %-10s %s\n' % (cmd, synopsis))
85 log('Other available commands:\n')
87 for c in sorted(os.listdir(cmdpath)):
88 if c.startswith(b'bup-') and c.find(b'.') < 0:
89 cname = fsdecode(c[4:])
90 if cname not in common:
91 cmds.add(c[4:].decode(errors='backslashreplace'))
92 # built-in commands take precedence
93 for _, name, _ in iter_modules(path=bup.cmd.__path__):
94 name = name.replace('_','-')
95 if name not in common:
98 log(columnate(sorted(cmds), ' '))
101 log("See 'bup help COMMAND' for more information on " +
102 "a specific command.\n")
107 def extract_argval(args):
108 """Assume args (all elements bytes) starts with a -x, --x, or --x=,
109 argument that requires a value and return that value and the remaining
110 args. Exit with an errror if the value is missing.
113 # Assumes that first arg is a valid arg
116 val = arg.split(b'=')[1]
118 usage('error: no value provided for %s option' % arg)
121 usage('error: no value provided for %s option' % arg)
122 return args[1], args[2:]
125 args = compat.get_argvb()
129 ## Parse global options
130 help_requested = None
136 if arg in (b'-?', b'--help'):
137 help_requested = True
139 elif arg in (b'-V', b'--version'):
140 subcmd = [b'version']
142 elif arg in (b'-D', b'--debug'):
144 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
146 elif arg == b'--profile':
149 elif arg in (b'-d', b'--bup-dir') or arg.startswith(b'--bup-dir='):
150 bup_dir, args = extract_argval(args)
151 elif arg == b'--import-py-module' or arg.startswith(b'--import-py-module='):
152 # Just need to skip it here
153 _, args = extract_argval(args)
154 elif arg.startswith(b'-'):
155 usage('error: unexpected option "%s"'
156 % arg.decode('ascii', 'backslashescape'))
162 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
164 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
172 if help_requested and subcmd[0] != b'help':
173 subcmd = [b'help'] + subcmd
175 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
176 subcmd = [b'help', subcmd[0]] + subcmd[2:]
178 subcmd_name = subcmd[0]
183 cmd_module = import_module('bup.cmd.'
184 + subcmd_name.decode('ascii').replace('-', '_'))
185 except ModuleNotFoundError as ex:
189 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
190 if not os.path.exists(subcmd[0]):
191 usage('error: unknown command "%s"' % path_msg(subcmd_name))
193 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
194 if subcmd_name in [b'mux', b'ftp', b'help']:
196 fix_stdout = not already_fixed and os.isatty(1)
197 fix_stderr = not already_fixed and os.isatty(2)
199 if fix_stdout or fix_stderr:
200 _ttymask = (fix_stdout and 1 or 0) + (fix_stderr and 2 or 0)
201 environ[b'BUP_FORCE_TTY'] = b'%d' % _ttymask
202 environ[b'BUP_TTY_WIDTH'] = b'%d' % _tty_width()
205 sep_rx = re.compile(br'([\r\n])')
207 def print_clean_line(dest, content, width, sep=None):
208 """Write some or all of content, followed by sep, to the dest fd after
209 padding the content with enough spaces to fill the current
210 terminal width or truncating it to the terminal width if sep is a
213 assert sep in (b'\r', b'\n', None)
219 assert not sep_rx.match(x)
220 content = b''.join(content)
221 if sep == b'\r' and len(content) > width:
222 content = content[:width]
223 os.write(dest, content)
224 if len(content) < width:
225 os.write(dest, b' ' * (width - len(content)))
229 def filter_output(srcs, dests):
230 """Transfer data from file descriptors in srcs to the corresponding
231 file descriptors in dests print_clean_line until all of the srcs
236 assert all(isinstance(x, int_types) for x in srcs)
237 assert len(srcs) == len(dests)
239 dest_for = dict(zip(srcs, dests))
244 ready_fds, _, _ = select.select(srcs, [], [])
247 buf = os.read(fd, 4096)
250 srcs = tuple([x for x in srcs if x is not fd])
251 print_clean_line(dest, pending.pop(fd, []), width)
253 split = sep_rx.split(buf)
254 while len(split) > 1:
255 content, sep = split[:2]
257 print_clean_line(dest,
258 pending.pop(fd, []) + [content],
261 assert len(split) == 1
263 pending.setdefault(fd, []).extend(split)
264 except BaseException as ex:
265 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
267 # Try to finish each of the streams
268 for fd, pending_items in compat.items(pending):
272 print_clean_line(dest, pending_items, width)
273 except (EnvironmentError, EOFError) as ex:
274 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
275 except BaseException as ex:
276 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
281 def import_and_run_main(module, args):
284 f = compile('module.main(args)', __file__, 'exec')
285 cProfile.runctx(f, globals(), locals())
290 def run_module_cmd(module, args):
291 if not (fix_stdout or fix_stderr):
292 import_and_run_main(module, args)
294 # Interpose filter_output between all attempts to write to the
295 # stdout/stderr and the real stdout/stderr (e.g. the fds that
296 # connect directly to the terminal) via a thread that runs
297 # filter_output in a pipeline.
300 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
301 filter_thread = filter_thread_started = None
306 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
307 real_out_fd = os.dup(sys.stdout.fileno())
308 os.dup2(stdout_pipe[1], sys.stdout.fileno())
309 srcs.append(stdout_pipe[0])
310 dests.append(real_out_fd)
313 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
314 real_err_fd = os.dup(sys.stderr.fileno())
315 os.dup2(stderr_pipe[1], sys.stderr.fileno())
316 srcs.append(stderr_pipe[0])
317 dests.append(real_err_fd)
319 filter_thread = Thread(name='output filter',
320 target=lambda : filter_output(srcs, dests))
321 filter_thread.start()
322 filter_thread_started = True
323 import_and_run_main(module, args)
324 except Exception as ex:
329 # Try to make sure that whatever else happens, we restore
330 # stdout and stderr here, if that's possible, so that we don't
331 # risk just losing some output.
333 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
334 except Exception as ex:
336 add_ex_ctx(ex, pending_ex)
338 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
339 except Exception as ex:
341 add_ex_ctx(ex, pending_ex)
344 stdout_pipe is not None and os.close(stdout_pipe[1])
345 except Exception as ex:
347 add_ex_ctx(ex, pending_ex)
349 stderr_pipe is not None and os.close(stderr_pipe[1])
350 except Exception as ex:
352 add_ex_ctx(ex, pending_ex)
355 except Exception as ex:
357 add_ex_ctx(ex, pending_ex)
360 # There's no point in trying to join unless we finished the finally block.
361 if filter_thread_started:
365 def run_subproc_cmd(args):
367 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
368 if not (fix_stdout or fix_stderr):
373 out = byte_stream(sys.stdout)
374 err = byte_stream(sys.stderr)
377 p = subprocess.Popen(c,
378 stdout=PIPE if fix_stdout else out,
379 stderr=PIPE if fix_stderr else err,
380 bufsize=4096, close_fds=True)
381 # Assume p will receive these signals and quit, which will
382 # then cause us to quit.
383 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
384 signal.signal(sig, signal.SIG_IGN)
389 srcs.append(p.stdout.fileno())
390 dests.append(out.fileno())
392 srcs.append(p.stderr.fileno())
393 dests.append(err.fileno())
394 filter_output(srcs, dests)
396 except BaseException as ex:
399 if p and p.poll() == None:
400 os.kill(p.pid, signal.SIGTERM)
402 except BaseException as kill_ex:
403 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
407 def run_subcmd(module, args):
409 run_module_cmd(module, args)
411 run_subproc_cmd(args)
414 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
416 if __name__ == "__main__":