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