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 args = compat.get_argvb()
114 ## Parse global options
115 help_requested = None
121 if arg in (b'-?', b'--help'):
122 help_requested = True
124 elif arg in (b'-V', b'--version'):
125 subcmd = [b'version']
127 elif arg in (b'-D', b'--debug'):
129 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
131 elif arg == b'--profile':
134 elif arg in (b'-d', b'--bup-dir'):
136 usage('error: no path provided for %s option' % arg)
139 elif arg == b'--import-py-module':
141 elif arg.startswith(b'-'):
142 usage('error: unexpected option "%s"'
143 % arg.decode('ascii', 'backslashescape'))
149 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
151 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
159 if help_requested and subcmd[0] != b'help':
160 subcmd = [b'help'] + subcmd
162 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
163 subcmd = [b'help', subcmd[0]] + subcmd[2:]
165 subcmd_name = subcmd[0]
170 cmd_module = import_module('bup.cmd.'
171 + subcmd_name.decode('ascii').replace('-', '_'))
172 except ModuleNotFoundError as ex:
176 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
177 if not os.path.exists(subcmd[0]):
178 usage('error: unknown command "%s"' % path_msg(subcmd_name))
180 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
181 if subcmd_name in [b'mux', b'ftp', b'help']:
183 fix_stdout = not already_fixed and os.isatty(1)
184 fix_stderr = not already_fixed and os.isatty(2)
186 if fix_stdout or fix_stderr:
187 tty_env = merge_dict(environ,
188 {b'BUP_FORCE_TTY': (b'%d'
189 % ((fix_stdout and 1 or 0)
190 + (fix_stderr and 2 or 0))),
191 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
196 sep_rx = re.compile(br'([\r\n])')
198 def print_clean_line(dest, content, width, sep=None):
199 """Write some or all of content, followed by sep, to the dest fd after
200 padding the content with enough spaces to fill the current
201 terminal width or truncating it to the terminal width if sep is a
204 assert sep in (b'\r', b'\n', None)
210 assert not sep_rx.match(x)
211 content = b''.join(content)
212 if sep == b'\r' and len(content) > width:
213 content = content[width:]
214 os.write(dest, content)
215 if len(content) < width:
216 os.write(dest, b' ' * (width - len(content)))
220 def filter_output(srcs, dests):
221 """Transfer data from file descriptors in srcs to the corresponding
222 file descriptors in dests print_clean_line until all of the srcs
227 assert all(type(x) in int_types for x in srcs)
228 assert all(type(x) in int_types for x in srcs)
229 assert len(srcs) == len(dests)
231 dest_for = dict(zip(srcs, dests))
236 ready_fds, _, _ = select.select(srcs, [], [])
239 buf = os.read(fd, 4096)
242 srcs = tuple([x for x in srcs if x is not fd])
243 print_clean_line(dest, pending.pop(fd, []), width)
245 split = sep_rx.split(buf)
246 while len(split) > 1:
247 content, sep = split[:2]
249 print_clean_line(dest,
250 pending.pop(fd, []) + [content],
253 assert len(split) == 1
255 pending.setdefault(fd, []).extend(split)
256 except BaseException as ex:
257 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
259 # Try to finish each of the streams
260 for fd, pending_items in compat.items(pending):
264 print_clean_line(dest, pending_items, width)
265 except (EnvironmentError, EOFError) as ex:
266 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
267 except BaseException as ex:
268 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
273 def import_and_run_main(module, args):
276 f = compile('module.main(args)', __file__, 'exec')
277 cProfile.runctx(f, globals(), locals())
282 def run_module_cmd(module, args):
283 if not (fix_stdout or fix_stderr):
284 import_and_run_main(module, args)
286 # Interpose filter_output between all attempts to write to the
287 # stdout/stderr and the real stdout/stderr (e.g. the fds that
288 # connect directly to the terminal) via a thread that runs
289 # filter_output in a pipeline.
292 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
293 filter_thread = filter_thread_started = None
298 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
299 real_out_fd = os.dup(sys.stdout.fileno())
300 os.dup2(stdout_pipe[1], sys.stdout.fileno())
301 srcs.append(stdout_pipe[0])
302 dests.append(real_out_fd)
305 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
306 real_err_fd = os.dup(sys.stderr.fileno())
307 os.dup2(stderr_pipe[1], sys.stderr.fileno())
308 srcs.append(stderr_pipe[0])
309 dests.append(real_err_fd)
311 filter_thread = Thread(name='output filter',
312 target=lambda : filter_output(srcs, dests))
313 filter_thread.start()
314 filter_thread_started = True
315 import_and_run_main(module, args)
316 except Exception as ex:
321 # Try to make sure that whatever else happens, we restore
322 # stdout and stderr here, if that's possible, so that we don't
323 # risk just losing some output.
325 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
326 except Exception as ex:
328 add_ex_ctx(ex, pending_ex)
330 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
331 except Exception as ex:
333 add_ex_ctx(ex, pending_ex)
336 stdout_pipe is not None and os.close(stdout_pipe[1])
337 except Exception as ex:
339 add_ex_ctx(ex, pending_ex)
341 stderr_pipe is not None and os.close(stderr_pipe[1])
342 except Exception as ex:
344 add_ex_ctx(ex, pending_ex)
347 except Exception as ex:
349 add_ex_ctx(ex, pending_ex)
352 # There's no point in trying to join unless we finished the finally block.
353 if filter_thread_started:
357 def run_subproc_cmd(args):
359 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
360 if not (fix_stdout or fix_stderr):
365 out = byte_stream(sys.stdout)
366 err = byte_stream(sys.stderr)
369 p = subprocess.Popen(c,
370 stdout=PIPE if fix_stdout else out,
371 stderr=PIPE if fix_stderr else err,
372 env=tty_env, bufsize=4096, close_fds=True)
373 # Assume p will receive these signals and quit, which will
374 # then cause us to quit.
375 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
376 signal.signal(sig, signal.SIG_IGN)
381 srcs.append(p.stdout.fileno())
382 dests.append(out.fileno())
384 srcs.append(p.stderr.fileno())
385 dests.append(err.fileno())
386 filter_output(srcs, dests)
388 except BaseException as ex:
391 if p and p.poll() == None:
392 os.kill(p.pid, signal.SIGTERM)
394 except BaseException as kill_ex:
395 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
399 def run_subcmd(module, args):
401 run_module_cmd(module, args)
403 run_subproc_cmd(args)
406 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
408 if __name__ == "__main__":