2 from __future__ import absolute_import, print_function
3 from importlib import import_module
4 from pkgutil import iter_modules
5 from subprocess import PIPE
6 from threading import Thread
7 import errno, getopt, os, re, select, signal, subprocess, sys
9 from bup import compat, path, helpers
10 from bup.compat import (
20 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
21 from bup.helpers import (
29 from bup.git import close_catpipes
30 from bup.io import byte_stream, path_msg
31 from bup.options import _tty_width
34 def maybe_import_early(argv):
35 """Scan argv and import any modules specified by --import-py-module."""
37 if argv[0] != '--import-py-module':
41 log("bup: --import-py-module must have an argument\n")
47 maybe_import_early(compat.get_argv())
51 cmdpath = path.cmddir()
53 # We manipulate the subcmds here as strings, but they must be ASCII
54 # compatible, since we're going to be looking for exactly
55 # b'bup-SUBCMD' to exec.
58 log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
59 '<command> [options...]\n\n')
61 ftp = 'Browse backup sets using an ftp-like client',
62 fsck = 'Check backup sets for damage and add redundancy information',
63 fuse = 'Mount your backup sets as a filesystem',
64 help = 'Print detailed help for the given command',
65 index = 'Create or display the index of files to back up',
66 on = 'Backup a remote machine to the local one',
67 restore = 'Extract files from a backup set',
68 save = 'Save files into a backup set (note: run "bup index" first)',
69 tag = 'Tag commits for easier access',
70 web = 'Launch a web server to examine backup sets',
73 log('Common commands:\n')
74 for cmd,synopsis in sorted(common.items()):
75 log(' %-10s %s\n' % (cmd, synopsis))
78 log('Other available commands:\n')
80 for c in sorted(os.listdir(cmdpath)):
81 if c.startswith(b'bup-') and c.find(b'.') < 0:
82 cname = fsdecode(c[4:])
83 if cname not in common:
84 cmds.add(c[4:].decode(errors='backslashreplace'))
85 # built-in commands take precedence
86 for _, name, _ in iter_modules(path=bup.cmd.__path__):
87 name = name.replace('_','-')
88 if name not in common:
91 log(columnate(sorted(cmds), ' '))
94 log("See 'bup help COMMAND' for more information on " +
95 "a specific command.\n")
100 argv = compat.get_argv()
104 # Handle global options.
106 optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
108 global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
109 except getopt.GetoptError as ex:
110 usage('error: %s' % ex.msg)
112 subcmd = [argv_bytes(x) for x in subcmd]
113 help_requested = None
117 for opt in global_args:
118 if opt[0] in ['-?', '--help']:
119 help_requested = True
120 elif opt[0] in ['-V', '--version']:
121 subcmd = [b'version']
122 elif opt[0] in ['-D', '--debug']:
124 environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
125 elif opt[0] in ['--profile']:
127 elif opt[0] in ['-d', '--bup-dir']:
128 bup_dir = argv_bytes(opt[1])
129 elif opt[0] == '--import-py-module':
132 usage('error: unexpected option "%s"' % opt[0])
134 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
136 environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
144 if help_requested and subcmd[0] != b'help':
145 subcmd = [b'help'] + subcmd
147 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
148 subcmd = [b'help', subcmd[0]] + subcmd[2:]
150 subcmd_name = subcmd[0]
155 cmd_module = import_module('bup.cmd.'
156 + subcmd_name.decode('ascii').replace('-', '_'))
157 except ModuleNotFoundError as ex:
161 subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
162 if not os.path.exists(subcmd[0]):
163 usage('error: unknown command "%s"' % path_msg(subcmd_name))
165 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
166 if subcmd_name in [b'mux', b'ftp', b'help']:
168 fix_stdout = not already_fixed and os.isatty(1)
169 fix_stderr = not already_fixed and os.isatty(2)
171 if fix_stdout or fix_stderr:
172 tty_env = merge_dict(environ,
173 {b'BUP_FORCE_TTY': (b'%d'
174 % ((fix_stdout and 1 or 0)
175 + (fix_stderr and 2 or 0))),
176 b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
181 sep_rx = re.compile(br'([\r\n])')
183 def print_clean_line(dest, content, width, sep=None):
184 """Write some or all of content, followed by sep, to the dest fd after
185 padding the content with enough spaces to fill the current
186 terminal width or truncating it to the terminal width if sep is a
189 assert sep in (b'\r', b'\n', None)
195 assert not sep_rx.match(x)
196 content = b''.join(content)
197 if sep == b'\r' and len(content) > width:
198 content = content[width:]
199 os.write(dest, content)
200 if len(content) < width:
201 os.write(dest, b' ' * (width - len(content)))
205 def filter_output(srcs, dests):
206 """Transfer data from file descriptors in srcs to the corresponding
207 file descriptors in dests print_clean_line until all of the srcs
212 assert all(type(x) in int_types for x in srcs)
213 assert all(type(x) in int_types for x in srcs)
214 assert len(srcs) == len(dests)
216 dest_for = dict(zip(srcs, dests))
221 ready_fds, _, _ = select.select(srcs, [], [])
224 buf = os.read(fd, 4096)
227 srcs = tuple([x for x in srcs if x is not fd])
228 print_clean_line(dest, pending.pop(fd, []), width)
230 split = sep_rx.split(buf)
231 while len(split) > 1:
232 content, sep = split[:2]
234 print_clean_line(dest,
235 pending.pop(fd, []) + [content],
238 assert len(split) == 1
240 pending.setdefault(fd, []).extend(split)
241 except BaseException as ex:
242 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
244 # Try to finish each of the streams
245 for fd, pending_items in compat.items(pending):
249 print_clean_line(dest, pending_items, width)
250 except (EnvironmentError, EOFError) as ex:
251 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
252 except BaseException as ex:
253 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
258 def import_and_run_main(module, args):
261 f = compile('module.main(args)', __file__, 'exec')
262 cProfile.runctx(f, globals(), locals())
267 def run_module_cmd(module, args):
268 if not (fix_stdout or fix_stderr):
269 import_and_run_main(module, args)
271 # Interpose filter_output between all attempts to write to the
272 # stdout/stderr and the real stdout/stderr (e.g. the fds that
273 # connect directly to the terminal) via a thread that runs
274 # filter_output in a pipeline.
277 real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
278 filter_thread = filter_thread_started = None
283 stdout_pipe = os.pipe() # monitored_by_filter, stdout_everyone_uses
284 real_out_fd = os.dup(sys.stdout.fileno())
285 os.dup2(stdout_pipe[1], sys.stdout.fileno())
286 srcs.append(stdout_pipe[0])
287 dests.append(real_out_fd)
290 stderr_pipe = os.pipe() # monitored_by_filter, stderr_everyone_uses
291 real_err_fd = os.dup(sys.stderr.fileno())
292 os.dup2(stderr_pipe[1], sys.stderr.fileno())
293 srcs.append(stderr_pipe[0])
294 dests.append(real_err_fd)
296 filter_thread = Thread(name='output filter',
297 target=lambda : filter_output(srcs, dests))
298 filter_thread.start()
299 filter_thread_started = True
300 import_and_run_main(module, args)
301 except Exception as ex:
306 # Try to make sure that whatever else happens, we restore
307 # stdout and stderr here, if that's possible, so that we don't
308 # risk just losing some output.
310 real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
311 except Exception as ex:
313 add_ex_ctx(ex, pending_ex)
315 real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
316 except Exception as ex:
318 add_ex_ctx(ex, pending_ex)
321 stdout_pipe is not None and os.close(stdout_pipe[1])
322 except Exception as ex:
324 add_ex_ctx(ex, pending_ex)
326 stderr_pipe is not None and os.close(stderr_pipe[1])
327 except Exception as ex:
329 add_ex_ctx(ex, pending_ex)
332 except Exception as ex:
334 add_ex_ctx(ex, pending_ex)
337 # There's no point in trying to join unless we finished the finally block.
338 if filter_thread_started:
342 def run_subproc_cmd(args):
344 c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
345 if not (fix_stdout or fix_stderr):
350 out = byte_stream(sys.stdout)
351 err = byte_stream(sys.stderr)
354 p = subprocess.Popen(c,
355 stdout=PIPE if fix_stdout else out,
356 stderr=PIPE if fix_stderr else err,
357 env=tty_env, bufsize=4096, close_fds=True)
358 # Assume p will receive these signals and quit, which will
359 # then cause us to quit.
360 for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
361 signal.signal(sig, signal.SIG_IGN)
366 srcs.append(p.stdout.fileno())
367 dests.append(out.fileno())
369 srcs.append(p.stderr.fileno())
370 dests.append(err.fileno())
371 filter_output(srcs, dests)
373 except BaseException as ex:
376 if p and p.poll() == None:
377 os.kill(p.pid, signal.SIGTERM)
379 except BaseException as kill_ex:
380 raise add_ex_ctx(add_ex_tb(kill_ex), ex)
384 def run_subcmd(module, args):
386 run_module_cmd(module, args)
388 run_subproc_cmd(args)
391 wrap_main(lambda : run_subcmd(cmd_module, subcmd))
393 if __name__ == "__main__":