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