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 (
27 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
28 from bup.helpers import (
34 from bup.git import close_catpipes
35 from bup.io import byte_stream, path_msg
36 from bup.options import _tty_width
39 def maybe_import_early(argv):
40 """Scan argv and import any modules specified by --import-py-module."""
42 if argv[0] != '--import-py-module':
46 log("bup: --import-py-module must have an argument\n")
52 maybe_import_early(compat.get_argv())
56 cmdpath = path.cmddir()
58 # We manipulate the subcmds here as strings, but they must be ASCII
59 # compatible, since we're going to be looking for exactly
60 # b'bup-SUBCMD' to exec.
63 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
64 '<command> [options...]\n\n')
66 ftp = 'Browse backup sets using an ftp-like client',
67 fsck = 'Check backup sets for damage and add redundancy information',
68 fuse = 'Mount your backup sets as a filesystem',
69 help = 'Print detailed help for the given command',
70 index = 'Create or display the index of files to back up',
71 on = 'Backup a remote machine to the local one',
72 restore = 'Extract files from a backup set',
73 save = 'Save files into a backup set (note: run "bup index" first)',
74 tag = 'Tag commits for easier access',
75 web = 'Launch a web server to examine backup sets',
78 log('Common commands:\n')
79 for cmd,synopsis in sorted(common.items()):
80 log(' %-10s %s\n' % (cmd, synopsis))
83 log('Other available commands:\n')
85 for c in sorted(os.listdir(cmdpath)):
86 if c.startswith(b'bup-') and c.find(b'.') < 0:
87 cname = fsdecode(c[4:])
88 if cname not in common:
89 cmds.add(c[4:].decode(errors='backslashreplace'))
90 # built-in commands take precedence
91 for _, name, _ in iter_modules(path=bup.cmd.__path__):
92 name = name.replace('_','-')
93 if name not in common:
96 log(columnate(sorted(cmds), ' '))
99 log("See 'bup help COMMAND' for more information on " +
100 "a specific command.\n")
105 def extract_argval(args):
106 """Assume args (all elements bytes) starts with a -x, --x, or --x=,
107 argument that requires a value and return that value and the remaining
108 args. Exit with an errror if the value is missing.
111 # Assumes that first arg is a valid arg
114 val = arg.split(b'=')[1]
116 usage('error: no value provided for %s option' % arg)
119 usage('error: no value provided for %s option' % arg)
120 return args[1], args[2:]
123 args = compat.get_argvb()
127 ## Parse global options
128 help_requested = None
134 if arg in (b'-?', b'--help'):
135 help_requested = True
137 elif arg in (b'-V', b'--version'):
138 subcmd = [b'version']
140 elif arg in (b'-D', b'--debug'):
142 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
144 elif arg == b'--profile':
147 elif arg in (b'-d', b'--bup-dir') or arg.startswith(b'--bup-dir='):
148 bup_dir, args = extract_argval(args)
149 elif arg == b'--import-py-module' or arg.startswith(b'--import-py-module='):
150 # Just need to skip it here
151 _, args = extract_argval(args)
152 elif arg.startswith(b'-'):
153 usage('error: unexpected option "%s"'
154 % arg.decode('ascii', 'backslashescape'))
160 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
162 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
170 if help_requested and subcmd[0] != b'help':
171 subcmd = [b'help'] + subcmd
173 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
174 subcmd = [b'help', subcmd[0]] + subcmd[2:]
176 subcmd_name = subcmd[0]
181 cmd_module = import_module('bup.cmd.'
182 + subcmd_name.decode('ascii').replace('-', '_'))
183 except ModuleNotFoundError as ex:
187 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
188 if not os.path.exists(subcmd[0]):
189 usage('error: unknown command "%s"' % path_msg(subcmd_name))
191 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
192 if subcmd_name in [b'mux', b'ftp', b'help']:
196 fix_stdout = not (already_fixed & 1) and os.isatty(1)
197 fix_stderr = not (already_fixed & 2) 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) 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 pending.items():
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__":