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']:
198 fix_stdout = not (already_fixed & 1) and os.isatty(1)
199 fix_stderr = not (already_fixed & 2) and os.isatty(2)
201 if fix_stdout or fix_stderr:
202 _ttymask = (fix_stdout and 1 or 0) + (fix_stderr and 2 or 0)
203 environ[b'BUP_FORCE_TTY'] = b'%d' % _ttymask
204 environ[b'BUP_TTY_WIDTH'] = b'%d' % _tty_width()
207 sep_rx = re.compile(br'([\r\n])')
209 def print_clean_line(dest, content, width, sep=None):
210 """Write some or all of content, followed by sep, to the dest fd after
211 padding the content with enough spaces to fill the current
212 terminal width or truncating it to the terminal width if sep is a
215 assert sep in (b'\r', b'\n', None)
221 assert not sep_rx.match(x)
222 content = b''.join(content)
223 if sep == b'\r' and len(content) > width:
224 content = content[:width]
225 os.write(dest, content)
226 if len(content) < width:
227 os.write(dest, b' ' * (width - len(content)))
231 def filter_output(srcs, dests):
232 """Transfer data from file descriptors in srcs to the corresponding
233 file descriptors in dests print_clean_line until all of the srcs
238 assert all(isinstance(x, int_types) for x in srcs)
239 assert len(srcs) == len(dests)
241 dest_for = dict(zip(srcs, dests))
246 ready_fds, _, _ = select.select(srcs, [], [])
249 buf = os.read(fd, 4096)
252 srcs = tuple([x for x in srcs if x is not fd])
253 print_clean_line(dest, pending.pop(fd, []), width)
255 split = sep_rx.split(buf)
256 while len(split) > 1:
257 content, sep = split[:2]
259 print_clean_line(dest,
260 pending.pop(fd, []) + [content],
263 assert len(split) == 1
265 pending.setdefault(fd, []).extend(split)
266 except BaseException as ex:
267 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
269 # Try to finish each of the streams
270 for fd, pending_items in compat.items(pending):
274 print_clean_line(dest, pending_items, width)
275 except (EnvironmentError, EOFError) as ex:
276 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
277 except BaseException as ex:
278 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
283 def import_and_run_main(module, args):
286 f = compile('module.main(args)', __file__, 'exec')
287 cProfile.runctx(f, globals(), locals())
292 def run_module_cmd(module, args):
293 if not (fix_stdout or fix_stderr):
294 import_and_run_main(module, args)
296 # Interpose filter_output between all attempts to write to the
297 # stdout/stderr and the real stdout/stderr (e.g. the fds that
298 # connect directly to the terminal) via a thread that runs
299 # filter_output in a pipeline.
302 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
303 filter_thread = filter_thread_started = None
308 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
309 real_out_fd = os.dup(sys.stdout.fileno())
310 os.dup2(stdout_pipe[1], sys.stdout.fileno())
311 srcs.append(stdout_pipe[0])
312 dests.append(real_out_fd)
315 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
316 real_err_fd = os.dup(sys.stderr.fileno())
317 os.dup2(stderr_pipe[1], sys.stderr.fileno())
318 srcs.append(stderr_pipe[0])
319 dests.append(real_err_fd)
321 filter_thread = Thread(name='output filter',
322 target=lambda : filter_output(srcs, dests))
323 filter_thread.start()
324 filter_thread_started = True
325 import_and_run_main(module, args)
326 except Exception as ex:
331 # Try to make sure that whatever else happens, we restore
332 # stdout and stderr here, if that's possible, so that we don't
333 # risk just losing some output.
335 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
336 except Exception as ex:
338 add_ex_ctx(ex, pending_ex)
340 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
341 except Exception as ex:
343 add_ex_ctx(ex, pending_ex)
346 stdout_pipe is not None and os.close(stdout_pipe[1])
347 except Exception as ex:
349 add_ex_ctx(ex, pending_ex)
351 stderr_pipe is not None and os.close(stderr_pipe[1])
352 except Exception as ex:
354 add_ex_ctx(ex, pending_ex)
357 except Exception as ex:
359 add_ex_ctx(ex, pending_ex)
362 # There's no point in trying to join unless we finished the finally block.
363 if filter_thread_started:
367 def run_subproc_cmd(args):
369 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
370 if not (fix_stdout or fix_stderr):
375 out = byte_stream(sys.stdout)
376 err = byte_stream(sys.stderr)
379 p = subprocess.Popen(c,
380 stdout=PIPE if fix_stdout else out,
381 stderr=PIPE if fix_stderr else err,
382 bufsize=4096, close_fds=True)
383 # Assume p will receive these signals and quit, which will
384 # then cause us to quit.
385 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
386 signal.signal(sig, signal.SIG_IGN)
391 srcs.append(p.stdout.fileno())
392 dests.append(out.fileno())
394 srcs.append(p.stderr.fileno())
395 dests.append(err.fileno())
396 filter_output(srcs, dests)
398 except BaseException as ex:
401 if p and p.poll() == None:
402 os.kill(p.pid, signal.SIGTERM)
404 except BaseException as kill_ex:
405 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
409 def run_subcmd(module, args):
411 run_module_cmd(module, args)
413 run_subproc_cmd(args)
416 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
418 if __name__ == "__main__":