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