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