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