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