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