]> arthur.barton.de Git - bup.git/blob - lib/cmd/bup
list-idx-cmd: copy to bup.cmd.list_idx
[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'gc',
196                            b'help',
197                            b'index',
198                            b'init',
199                            b'join',
200                            b'ls',
201                            b'margin',
202                            b'memtest',
203                            b'meta',
204                            b'midx',
205                            b'random',
206                            b'rm',
207                            b'split',
208                            b'tag',
209                            b'tick',
210                            b'version',
211                            b'web',
212                            b'xstat'):
213         raise ModuleNotFoundError()
214     cmd_module = import_module('bup.cmd.'
215                                + subcmd_name.decode('ascii').replace('-', '_'))
216 except ModuleNotFoundError as ex:
217     cmd_module = None
218
219 if not cmd_module:
220     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
221     if not os.path.exists(subcmd[0]):
222         subcmd[0] = b'%s/%s.py' % (transition_cmdpath,
223                                    subcmd_name.replace(b'-', b'_'))
224     if not os.path.exists(subcmd[0]):
225         usage('error: unknown command "%s"' % path_msg(subcmd_name))
226
227 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
228 if subcmd_name in [b'mux', b'ftp', b'help']:
229     already_fixed = True
230 fix_stdout = not already_fixed and os.isatty(1)
231 fix_stderr = not already_fixed and os.isatty(2)
232
233 if fix_stdout or fix_stderr:
234     tty_env = merge_dict(environ,
235                          {b'BUP_FORCE_TTY': (b'%d'
236                                              % ((fix_stdout and 1 or 0)
237                                                 + (fix_stderr and 2 or 0))),
238                           b'BUP_TTY_WIDTH': b'%d' % _tty_width(), })
239 else:
240     tty_env = environ
241
242
243 sep_rx = re.compile(br'([\r\n])')
244
245 def print_clean_line(dest, content, width, sep=None):
246     """Write some or all of content, followed by sep, to the dest fd after
247     padding the content with enough spaces to fill the current
248     terminal width or truncating it to the terminal width if sep is a
249     carriage return."""
250     global sep_rx
251     assert sep in (b'\r', b'\n', None)
252     if not content:
253         if sep:
254             os.write(dest, sep)
255         return
256     for x in content:
257         assert not sep_rx.match(x)
258     content = b''.join(content)
259     if sep == b'\r' and len(content) > width:
260         content = content[width:]
261     os.write(dest, content)
262     if len(content) < width:
263         os.write(dest, b' ' * (width - len(content)))
264     if sep:
265         os.write(dest, sep)
266
267 def filter_output(srcs, dests):
268     """Transfer data from file descriptors in srcs to the corresponding
269     file descriptors in dests print_clean_line until all of the srcs
270     have closed.
271
272     """
273     global sep_rx
274     assert all(type(x) in int_types for x in srcs)
275     assert all(type(x) in int_types for x in srcs)
276     assert len(srcs) == len(dests)
277     srcs = tuple(srcs)
278     dest_for = dict(zip(srcs, dests))
279     pending = {}
280     pending_ex = None
281     try:
282         while srcs:
283             ready_fds, _, _ = select.select(srcs, [], [])
284             width = tty_width()
285             for fd in ready_fds:
286                 buf = os.read(fd, 4096)
287                 dest = dest_for[fd]
288                 if not buf:
289                     srcs = tuple([x for x in srcs if x is not fd])
290                     print_clean_line(dest, pending.pop(fd, []), width)
291                 else:
292                     split = sep_rx.split(buf)
293                     while len(split) > 1:
294                         content, sep = split[:2]
295                         split = split[2:]
296                         print_clean_line(dest,
297                                          pending.pop(fd, []) + [content],
298                                          width,
299                                          sep)
300                     assert len(split) == 1
301                     if split[0]:
302                         pending.setdefault(fd, []).extend(split)
303     except BaseException as ex:
304         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
305     try:
306         # Try to finish each of the streams
307         for fd, pending_items in compat.items(pending):
308             dest = dest_for[fd]
309             width = tty_width()
310             try:
311                 print_clean_line(dest, pending_items, width)
312             except (EnvironmentError, EOFError) as ex:
313                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
314     except BaseException as ex:
315         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
316     if pending_ex:
317         raise pending_ex
318
319
320 def import_and_run_main(module, args):
321     if do_profile:
322         import cProfile
323         f = compile('module.main(args)', __file__, 'exec')
324         cProfile.runctx(f, globals(), locals())
325     else:
326         module.main(args)
327
328
329 def run_module_cmd(module, args):
330     if not (fix_stdout or fix_stderr):
331         import_and_run_main(module, args)
332         return
333     # Interpose filter_output between all attempts to write to the
334     # stdout/stderr and the real stdout/stderr (e.g. the fds that
335     # connect directly to the terminal) via a thread that runs
336     # filter_output in a pipeline.
337     srcs = []
338     dests = []
339     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
340     filter_thread = filter_thread_started = None
341     pending_ex = None
342     try:
343         if fix_stdout:
344             sys.stdout.flush()
345             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
346             real_out_fd = os.dup(sys.stdout.fileno())
347             os.dup2(stdout_pipe[1], sys.stdout.fileno())
348             srcs.append(stdout_pipe[0])
349             dests.append(real_out_fd)
350         if fix_stderr:
351             sys.stderr.flush()
352             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
353             real_err_fd = os.dup(sys.stderr.fileno())
354             os.dup2(stderr_pipe[1], sys.stderr.fileno())
355             srcs.append(stderr_pipe[0])
356             dests.append(real_err_fd)
357
358         filter_thread = Thread(name='output filter',
359                                target=lambda : filter_output(srcs, dests))
360         filter_thread.start()
361         filter_thread_started = True
362         import_and_run_main(module, args)
363     except Exception as ex:
364         add_ex_tb(ex)
365         pending_ex = ex
366         raise
367     finally:
368         # Try to make sure that whatever else happens, we restore
369         # stdout and stderr here, if that's possible, so that we don't
370         # risk just losing some output.
371         try:
372             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
373         except Exception as ex:
374             add_ex_tb(ex)
375             add_ex_ctx(ex, pending_ex)
376         try:
377             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
378         except Exception as ex:
379             add_ex_tb(ex)
380             add_ex_ctx(ex, pending_ex)
381         # Kick filter loose
382         try:
383             stdout_pipe is not None and os.close(stdout_pipe[1])
384         except Exception as ex:
385             add_ex_tb(ex)
386             add_ex_ctx(ex, pending_ex)
387         try:
388             stderr_pipe is not None and os.close(stderr_pipe[1])
389         except Exception as ex:
390             add_ex_tb(ex)
391             add_ex_ctx(ex, pending_ex)
392         try:
393             close_catpipes()
394         except Exception as ex:
395             add_ex_tb(ex)
396             add_ex_ctx(ex, pending_ex)
397     if pending_ex:
398         raise pending_ex
399     # There's no point in trying to join unless we finished the finally block.
400     if filter_thread_started:
401         filter_thread.join()
402
403
404 def run_subproc_cmd(args):
405
406     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
407     if not (fix_stdout or fix_stderr):
408         os.execvp(c[0], c)
409
410     sys.stdout.flush()
411     sys.stderr.flush()
412     out = byte_stream(sys.stdout)
413     err = byte_stream(sys.stderr)
414     p = None
415     try:
416         p = subprocess.Popen(c,
417                              stdout=PIPE if fix_stdout else out,
418                              stderr=PIPE if fix_stderr else err,
419                              env=tty_env, bufsize=4096, close_fds=True)
420         # Assume p will receive these signals and quit, which will
421         # then cause us to quit.
422         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
423             signal.signal(sig, signal.SIG_IGN)
424
425         srcs = []
426         dests = []
427         if fix_stdout:
428             srcs.append(p.stdout.fileno())
429             dests.append(out.fileno())
430         if fix_stderr:
431             srcs.append(p.stderr.fileno())
432             dests.append(err.fileno())
433         filter_output(srcs, dests)
434         return p.wait()
435     except BaseException as ex:
436         add_ex_tb(ex)
437         try:
438             if p and p.poll() == None:
439                 os.kill(p.pid, signal.SIGTERM)
440                 p.wait()
441         except BaseException as kill_ex:
442             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
443         raise ex
444
445
446 def run_subcmd(module, args):
447     if module:
448         run_module_cmd(module, args)
449     else:
450         run_subproc_cmd(args)
451
452
453 wrap_main(lambda : run_subcmd(cmd_module, subcmd))