]> arthur.barton.de Git - bup.git/blob - lib/cmd/bup
e474b341c899e4087b6d794170fb79812ee21308
[bup.git] / lib / cmd / bup
1 #!/bin/sh
2 """": # -*-python-*-
3 set -e
4 # https://sourceware.org/bugzilla/show_bug.cgi?id=26034
5 export "BUP_ARGV_0"="$0"
6 arg_i=1
7 for arg in "$@"; do
8     export "BUP_ARGV_${arg_i}"="$arg"
9     shift
10     arg_i=$((arg_i + 1))
11 done
12 # Here to end of preamble replaced during install
13 # Find our directory
14 top="$(pwd)"
15 cmdpath="$0"
16 # loop because macos doesn't have recursive readlink/realpath utils
17 while test -L "$cmdpath"; do
18     link="$(readlink "$cmdpath")"
19     cd "$(dirname "$cmdpath")"
20     cmdpath="$link"
21 done
22 script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)"
23 cd "$top"
24 exec "$script_home/../../config/bin/python" "$0"
25 """
26 # end of bup preamble
27
28 from __future__ import absolute_import, print_function
29
30 import os, sys
31 sys.path[:0] = [os.path.dirname(os.path.realpath(__file__)) + '/..']
32
33 from importlib import import_module
34 from pkgutil import iter_modules
35 from subprocess import PIPE
36 from threading import Thread
37 import errno, getopt, os, re, select, signal, subprocess, sys
38
39 from bup import compat, path, helpers
40 from bup.compat import (
41     ModuleNotFoundError,
42     add_ex_ctx,
43     add_ex_tb,
44     argv_bytes,
45     environ,
46     fsdecode,
47     int_types,
48     wrap_main
49 )
50 from bup.compat import add_ex_tb, add_ex_ctx, argv_bytes, wrap_main
51 from bup.helpers import (
52     columnate,
53     debug1,
54     handle_ctrl_c,
55     log,
56     merge_dict,
57     tty_width
58 )
59 from bup.git import close_catpipes
60 from bup.io import byte_stream, path_msg
61 from bup.options import _tty_width
62 import bup.cmd
63
64 def maybe_import_early(argv):
65     """Scan argv and import any modules specified by --import-py-module."""
66     while argv:
67         if argv[0] != '--import-py-module':
68             argv = argv[1:]
69             continue
70         if len(argv) < 2:
71             log("bup: --import-py-module must have an argument\n")
72             exit(2)
73         mod = argv[1]
74         import_module(mod)
75         argv = argv[2:]
76
77 maybe_import_early(compat.argv)
78
79 handle_ctrl_c()
80
81 cmdpath = path.cmddir()
82
83 # Remove once we finish the internal command transition
84 transition_cmdpath = path.libdir() + b'/bup/cmd'
85
86 # We manipulate the subcmds here as strings, but they must be ASCII
87 # compatible, since we're going to be looking for exactly
88 # b'bup-SUBCMD' to exec.
89
90 def usage(msg=""):
91     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
92         '<command> [options...]\n\n')
93     common = dict(
94         ftp = 'Browse backup sets using an ftp-like client',
95         fsck = 'Check backup sets for damage and add redundancy information',
96         fuse = 'Mount your backup sets as a filesystem',
97         help = 'Print detailed help for the given command',
98         index = 'Create or display the index of files to back up',
99         on = 'Backup a remote machine to the local one',
100         restore = 'Extract files from a backup set',
101         save = 'Save files into a backup set (note: run "bup index" first)',
102         tag = 'Tag commits for easier access',
103         web = 'Launch a web server to examine backup sets',
104     )
105
106     log('Common commands:\n')
107     for cmd,synopsis in sorted(common.items()):
108         log('    %-10s %s\n' % (cmd, synopsis))
109     log('\n')
110     
111     log('Other available commands:\n')
112     cmds = set()
113     for c in sorted(os.listdir(cmdpath)):
114         if c.startswith(b'bup-') and c.find(b'.') < 0:
115             cname = fsdecode(c[4:])
116             if cname not in common:
117                 cmds.add(c[4:].decode(errors='backslashreplace'))
118     # built-in commands take precedence
119     for _, name, _ in iter_modules(path=bup.cmd.__path__):
120         name = name.replace('_','-')
121         if name not in common:
122             cmds.add(name)
123
124     log(columnate(sorted(cmds), '    '))
125     log('\n')
126     
127     log("See 'bup help COMMAND' for more information on " +
128         "a specific command.\n")
129     if msg:
130         log("\n%s\n" % msg)
131     sys.exit(99)
132
133 argv = compat.argv
134 if len(argv) < 2:
135     usage()
136
137 # Handle global options.
138 try:
139     optspec = ['help', 'version', 'debug', 'profile', 'bup-dir=',
140                'import-py-module=']
141     global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec)
142 except getopt.GetoptError as ex:
143     usage('error: %s' % ex.msg)
144
145 subcmd = [argv_bytes(x) for x in subcmd]
146 help_requested = None
147 do_profile = False
148 bup_dir = None
149
150 for opt in global_args:
151     if opt[0] in ['-?', '--help']:
152         help_requested = True
153     elif opt[0] in ['-V', '--version']:
154         subcmd = [b'version']
155     elif opt[0] in ['-D', '--debug']:
156         helpers.buglvl += 1
157         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
158     elif opt[0] in ['--profile']:
159         do_profile = True
160     elif opt[0] in ['-d', '--bup-dir']:
161         bup_dir = argv_bytes(opt[1])
162     elif opt[0] == '--import-py-module':
163         pass
164     else:
165         usage('error: unexpected option "%s"' % opt[0])
166
167 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
168 if bup_dir:
169     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
170
171 if len(subcmd) == 0:
172     if help_requested:
173         subcmd = [b'help']
174     else:
175         usage()
176
177 if help_requested and subcmd[0] != b'help':
178     subcmd = [b'help'] + subcmd
179
180 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
181     subcmd = [b'help', subcmd[0]] + subcmd[2:]
182
183 subcmd_name = subcmd[0]
184 if not subcmd_name:
185     usage()
186
187 try:
188     if subcmd_name not in (b'bloom',
189                            b'cat-file',
190                            b'daemon',
191                            b'drecurse',
192                            b'damage',
193                            b'features',
194                            b'ftp',
195                            b'fsck',
196                            b'fuse',
197                            b'gc',
198                            b'get',
199                            b'help',
200                            b'import-duplicity',
201                            b'index',
202                            b'init',
203                            b'join',
204                            b'list-idx',
205                            b'ls',
206                            b'margin',
207                            b'memtest',
208                            b'meta',
209                            b'midx',
210                            b'mux',
211                            b'on',
212                            b'on--server',
213                            b'prune-older',
214                            b'random',
215                            b'restore',
216                            b'rm',
217                            b'save',
218                            b'server',
219                            b'split',
220                            b'tag',
221                            b'tick',
222                            b'version',
223                            b'web',
224                            b'xstat'):
225         raise ModuleNotFoundError()
226     cmd_module = import_module('bup.cmd.'
227                                + subcmd_name.decode('ascii').replace('-', '_'))
228 except ModuleNotFoundError as ex:
229     cmd_module = None
230
231 if not cmd_module:
232     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
233     if not os.path.exists(subcmd[0]):
234         subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
235                                    subcmd_name.replace(b'-', b'_'))
236     if not os.path.exists(subcmd[0]):
237         usage('error: unknown command "%s"' % path_msg(subcmd_name))
238
239 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
240 if subcmd_name in [b'mux', b'ftp', b'help']:
241     already_fixed = True
242 fix_stdout = not already_fixed and os.isatty(1)
243 fix_stderr = not already_fixed and os.isatty(2)
244
245 if fix_stdout or fix_stderr:
246     tty_env = merge_dict(environ,
247                          {b'BUP_FORCE_TTY': (b'%d'
248                                              % ((fix_stdout and 1 or 0)
249                                                 + (fix_stderr and 2 or 0))),
250                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
251 else:
252     tty_env = environ
253
254
255 sep_rx = re.compile(br'([\r\n])')
256
257 def print_clean_line(dest, content, width, sep=None):
258     """Write some or all of content, followed by sep, to the dest fd after
259     padding the content with enough spaces to fill the current
260     terminal width or truncating it to the terminal width if sep is a
261     carriage return."""
262     global sep_rx
263     assert sep in (b'\r', b'\n', None)
264     if not content:
265         if sep:
266             os.write(dest, sep)
267         return
268     for x in content:
269         assert not sep_rx.match(x)
270     content = b''.join(content)
271     if sep == b'\r' and len(content) > width:
272         content = content[width:]
273     os.write(dest, content)
274     if len(content) < width:
275         os.write(dest, b' ' * (width - len(content)))
276     if sep:
277         os.write(dest, sep)
278
279 def filter_output(srcs, dests):
280     """Transfer data from file descriptors in srcs to the corresponding
281     file descriptors in dests print_clean_line until all of the srcs
282     have closed.
283
284     """
285     global sep_rx
286     assert all(type(x) in int_types for x in srcs)
287     assert all(type(x) in int_types for x in srcs)
288     assert len(srcs) == len(dests)
289     srcs = tuple(srcs)
290     dest_for = dict(zip(srcs, dests))
291     pending = {}
292     pending_ex = None
293     try:
294         while srcs:
295             ready_fds, _, _ = select.select(srcs, [], [])
296             width = tty_width()
297             for fd in ready_fds:
298                 buf = os.read(fd, 4096)
299                 dest = dest_for[fd]
300                 if not buf:
301                     srcs = tuple([x for x in srcs if x is not fd])
302                     print_clean_line(dest, pending.pop(fd, []), width)
303                 else:
304                     split = sep_rx.split(buf)
305                     while len(split) > 1:
306                         content, sep = split[:2]
307                         split = split[2:]
308                         print_clean_line(dest,
309                                          pending.pop(fd, []) + [content],
310                                          width,
311                                          sep)
312                     assert len(split) == 1
313                     if split[0]:
314                         pending.setdefault(fd, []).extend(split)
315     except BaseException as ex:
316         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
317     try:
318         # Try to finish each of the streams
319         for fd, pending_items in compat.items(pending):
320             dest = dest_for[fd]
321             width = tty_width()
322             try:
323                 print_clean_line(dest, pending_items, width)
324             except (EnvironmentError, EOFError) as ex:
325                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
326     except BaseException as ex:
327         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
328     if pending_ex:
329         raise pending_ex
330
331
332 def import_and_run_main(module, args):
333     if do_profile:
334         import cProfile
335         f = compile('module.main(args)', __file__, 'exec')
336         cProfile.runctx(f, globals(), locals())
337     else:
338         module.main(args)
339
340
341 def run_module_cmd(module, args):
342     if not (fix_stdout or fix_stderr):
343         import_and_run_main(module, args)
344         return
345     # Interpose filter_output between all attempts to write to the
346     # stdout/stderr and the real stdout/stderr (e.g. the fds that
347     # connect directly to the terminal) via a thread that runs
348     # filter_output in a pipeline.
349     srcs = []
350     dests = []
351     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
352     filter_thread = filter_thread_started = None
353     pending_ex = None
354     try:
355         if fix_stdout:
356             sys.stdout.flush()
357             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
358             real_out_fd = os.dup(sys.stdout.fileno())
359             os.dup2(stdout_pipe[1], sys.stdout.fileno())
360             srcs.append(stdout_pipe[0])
361             dests.append(real_out_fd)
362         if fix_stderr:
363             sys.stderr.flush()
364             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
365             real_err_fd = os.dup(sys.stderr.fileno())
366             os.dup2(stderr_pipe[1], sys.stderr.fileno())
367             srcs.append(stderr_pipe[0])
368             dests.append(real_err_fd)
369
370         filter_thread = Thread(name='output filter',
371                                target=lambda : filter_output(srcs, dests))
372         filter_thread.start()
373         filter_thread_started = True
374         import_and_run_main(module, args)
375     except Exception as ex:
376         add_ex_tb(ex)
377         pending_ex = ex
378         raise
379     finally:
380         # Try to make sure that whatever else happens, we restore
381         # stdout and stderr here, if that's possible, so that we don't
382         # risk just losing some output.
383         try:
384             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
385         except Exception as ex:
386             add_ex_tb(ex)
387             add_ex_ctx(ex, pending_ex)
388         try:
389             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
390         except Exception as ex:
391             add_ex_tb(ex)
392             add_ex_ctx(ex, pending_ex)
393         # Kick filter loose
394         try:
395             stdout_pipe is not None and os.close(stdout_pipe[1])
396         except Exception as ex:
397             add_ex_tb(ex)
398             add_ex_ctx(ex, pending_ex)
399         try:
400             stderr_pipe is not None and os.close(stderr_pipe[1])
401         except Exception as ex:
402             add_ex_tb(ex)
403             add_ex_ctx(ex, pending_ex)
404         try:
405             close_catpipes()
406         except Exception as ex:
407             add_ex_tb(ex)
408             add_ex_ctx(ex, pending_ex)
409     if pending_ex:
410         raise pending_ex
411     # There's no point in trying to join unless we finished the finally block.
412     if filter_thread_started:
413         filter_thread.join()
414
415
416 def run_subproc_cmd(args):
417
418     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
419     if not (fix_stdout or fix_stderr):
420         os.execvp(c[0], c)
421
422     sys.stdout.flush()
423     sys.stderr.flush()
424     out = byte_stream(sys.stdout)
425     err = byte_stream(sys.stderr)
426     p = None
427     try:
428         p = subprocess.Popen(c,
429                              stdout=PIPE if fix_stdout else out,
430                              stderr=PIPE if fix_stderr else err,
431                              env=tty_env, bufsize=4096, close_fds=True)
432         # Assume p will receive these signals and quit, which will
433         # then cause us to quit.
434         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
435             signal.signal(sig, signal.SIG_IGN)
436
437         srcs = []
438         dests = []
439         if fix_stdout:
440             srcs.append(p.stdout.fileno())
441             dests.append(out.fileno())
442         if fix_stderr:
443             srcs.append(p.stderr.fileno())
444             dests.append(err.fileno())
445         filter_output(srcs, dests)
446         return p.wait()
447     except BaseException as ex:
448         add_ex_tb(ex)
449         try:
450             if p and p.poll() == None:
451                 os.kill(p.pid, signal.SIGTERM)
452                 p.wait()
453         except BaseException as kill_ex:
454             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
455         raise ex
456
457
458 def run_subcmd(module, args):
459     if module:
460         run_module_cmd(module, args)
461     else:
462         run_subproc_cmd(args)
463
464
465 wrap_main(lambda : run_subcmd(cmd_module, subcmd))