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