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