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