]> arthur.barton.de Git - bup.git/blob - lib/bup/main.py
Drop vestigial compat.range
[bup.git] / lib / bup / main.py
1
2 from __future__ import absolute_import, print_function
3
4 import bup_main, os, sys
5 if bup_main.env_pythonpath:
6     if sys.version_info[0] < 3:
7         os.environ['PYTHONPATH'] = bup_main.env_pythonpath
8     else:
9         os.environb[b'PYTHONPATH'] = bup_main.env_pythonpath
10 else:
11     del os.environ['PYTHONPATH']
12
13 from importlib import import_module
14 from pkgutil import iter_modules
15 from subprocess import PIPE
16 from threading import Thread
17 import re, select, signal, subprocess
18
19 from bup import compat, path, helpers
20 from bup.compat import (
21     add_ex_ctx,
22     add_ex_tb,
23     environ,
24     fsdecode,
25     int_types,
26     wrap_main
27 )
28 from bup.compat import add_ex_tb, add_ex_ctx, wrap_main
29 from bup.helpers import (
30     columnate,
31     handle_ctrl_c,
32     log,
33     tty_width
34 )
35 from bup.git import close_catpipes
36 from bup.io import byte_stream, path_msg
37 from bup.options import _tty_width
38 import bup.cmd
39
40 def maybe_import_early(argv):
41     """Scan argv and import any modules specified by --import-py-module."""
42     while argv:
43         if argv[0] != '--import-py-module':
44             argv = argv[1:]
45             continue
46         if len(argv) < 2:
47             log("bup: --import-py-module must have an argument\n")
48             exit(2)
49         mod = argv[1]
50         import_module(mod)
51         argv = argv[2:]
52
53 maybe_import_early(compat.get_argv())
54
55 handle_ctrl_c()
56
57 cmdpath = path.cmddir()
58
59 # We manipulate the subcmds here as strings, but they must be ASCII
60 # compatible, since we're going to be looking for exactly
61 # b'bup-SUBCMD' to exec.
62
63 def usage(msg=""):
64     log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] '
65         '<command> [options...]\n\n')
66     common = dict(
67         ftp = 'Browse backup sets using an ftp-like client',
68         fsck = 'Check backup sets for damage and add redundancy information',
69         fuse = 'Mount your backup sets as a filesystem',
70         help = 'Print detailed help for the given command',
71         index = 'Create or display the index of files to back up',
72         on = 'Backup a remote machine to the local one',
73         restore = 'Extract files from a backup set',
74         save = 'Save files into a backup set (note: run "bup index" first)',
75         tag = 'Tag commits for easier access',
76         web = 'Launch a web server to examine backup sets',
77     )
78
79     log('Common commands:\n')
80     for cmd,synopsis in sorted(common.items()):
81         log('    %-10s %s\n' % (cmd, synopsis))
82     log('\n')
83
84     log('Other available commands:\n')
85     cmds = set()
86     for c in sorted(os.listdir(cmdpath)):
87         if c.startswith(b'bup-') and c.find(b'.') < 0:
88             cname = fsdecode(c[4:])
89             if cname not in common:
90                 cmds.add(c[4:].decode(errors='backslashreplace'))
91     # built-in commands take precedence
92     for _, name, _ in iter_modules(path=bup.cmd.__path__):
93         name = name.replace('_','-')
94         if name not in common:
95             cmds.add(name)
96
97     log(columnate(sorted(cmds), '    '))
98     log('\n')
99
100     log("See 'bup help COMMAND' for more information on " +
101         "a specific command.\n")
102     if msg:
103         log("\n%s\n" % msg)
104     sys.exit(99)
105
106 def extract_argval(args):
107     """Assume args (all elements bytes) starts with a -x, --x, or --x=,
108 argument that requires a value and return that value and the remaining
109 args.  Exit with an errror if the value is missing.
110
111     """
112     # Assumes that first arg is a valid arg
113     arg = args[0]
114     if b'=' in arg:
115         val = arg.split(b'=')[1]
116         if not val:
117             usage('error: no value provided for %s option' % arg)
118         return val, args[1:]
119     if len(args) < 2:
120         usage('error: no value provided for %s option' % arg)
121     return args[1], args[2:]
122
123
124 args = compat.get_argvb()
125 if len(args) < 2:
126     usage()
127
128 ## Parse global options
129 help_requested = None
130 do_profile = False
131 bup_dir = None
132 args = args[1:]
133 while args:
134     arg = args[0]
135     if arg in (b'-?', b'--help'):
136         help_requested = True
137         args = args[1:]
138     elif arg in (b'-V', b'--version'):
139         subcmd = [b'version']
140         args = args[1:]
141     elif arg in (b'-D', b'--debug'):
142         helpers.buglvl += 1
143         environ[b'BUP_DEBUG'] = b'%d' % helpers.buglvl
144         args = args[1:]
145     elif arg == b'--profile':
146         do_profile = True
147         args = args[1:]
148     elif arg in (b'-d', b'--bup-dir') or arg.startswith(b'--bup-dir='):
149         bup_dir, args = extract_argval(args)
150     elif arg == b'--import-py-module' or arg.startswith(b'--import-py-module='):
151         # Just need to skip it here
152         _, args = extract_argval(args)
153     elif arg.startswith(b'-'):
154         usage('error: unexpected option "%s"'
155               % arg.decode('ascii', 'backslashescape'))
156     else:
157         break
158
159 subcmd = args
160
161 # Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.).
162 if bup_dir:
163     environ[b'BUP_DIR'] = os.path.abspath(bup_dir)
164
165 if len(subcmd) == 0:
166     if help_requested:
167         subcmd = [b'help']
168     else:
169         usage()
170
171 if help_requested and subcmd[0] != b'help':
172     subcmd = [b'help'] + subcmd
173
174 if len(subcmd) > 1 and subcmd[1] == b'--help' and subcmd[0] != b'help':
175     subcmd = [b'help', subcmd[0]] + subcmd[2:]
176
177 subcmd_name = subcmd[0]
178 if not subcmd_name:
179     usage()
180
181 try:
182     cmd_module = import_module('bup.cmd.'
183                                + subcmd_name.decode('ascii').replace('-', '_'))
184 except ModuleNotFoundError as ex:
185     cmd_module = None
186
187 if not cmd_module:
188     subcmd[0] = os.path.join(cmdpath, b'bup-' + subcmd_name)
189     if not os.path.exists(subcmd[0]):
190         usage('error: unknown command "%s"' % path_msg(subcmd_name))
191
192 already_fixed = int(environ.get(b'BUP_FORCE_TTY', 0))
193 if subcmd_name in [b'mux', b'ftp', b'help']:
194     fix_stdout = False
195     fix_stderr = False
196 else:
197     fix_stdout = not (already_fixed & 1) and os.isatty(1)
198     fix_stderr = not (already_fixed & 2) and os.isatty(2)
199
200 if fix_stdout or fix_stderr:
201     _ttymask = (fix_stdout and 1 or 0) + (fix_stderr and 2 or 0)
202     environ[b'BUP_FORCE_TTY'] = b'%d' % _ttymask
203     environ[b'BUP_TTY_WIDTH'] = b'%d' % _tty_width()
204
205
206 sep_rx = re.compile(br'([\r\n])')
207
208 def print_clean_line(dest, content, width, sep=None):
209     """Write some or all of content, followed by sep, to the dest fd after
210     padding the content with enough spaces to fill the current
211     terminal width or truncating it to the terminal width if sep is a
212     carriage return."""
213     global sep_rx
214     assert sep in (b'\r', b'\n', None)
215     if not content:
216         if sep:
217             os.write(dest, sep)
218         return
219     for x in content:
220         assert not sep_rx.match(x)
221     content = b''.join(content)
222     if sep == b'\r' and len(content) > width:
223         content = content[:width]
224     os.write(dest, content)
225     if len(content) < width:
226         os.write(dest, b' ' * (width - len(content)))
227     if sep:
228         os.write(dest, sep)
229
230 def filter_output(srcs, dests):
231     """Transfer data from file descriptors in srcs to the corresponding
232     file descriptors in dests print_clean_line until all of the srcs
233     have closed.
234
235     """
236     global sep_rx
237     assert all(isinstance(x, int_types) for x in srcs)
238     assert len(srcs) == len(dests)
239     srcs = tuple(srcs)
240     dest_for = dict(zip(srcs, dests))
241     pending = {}
242     pending_ex = None
243     try:
244         while srcs:
245             ready_fds, _, _ = select.select(srcs, [], [])
246             width = tty_width()
247             for fd in ready_fds:
248                 buf = os.read(fd, 4096)
249                 dest = dest_for[fd]
250                 if not buf:
251                     srcs = tuple([x for x in srcs if x is not fd])
252                     print_clean_line(dest, pending.pop(fd, []), width)
253                 else:
254                     split = sep_rx.split(buf)
255                     while len(split) > 1:
256                         content, sep = split[:2]
257                         split = split[2:]
258                         print_clean_line(dest,
259                                          pending.pop(fd, []) + [content],
260                                          width,
261                                          sep)
262                     assert len(split) == 1
263                     if split[0]:
264                         pending.setdefault(fd, []).extend(split)
265     except BaseException as ex:
266         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
267     try:
268         # Try to finish each of the streams
269         for fd, pending_items in compat.items(pending):
270             dest = dest_for[fd]
271             width = tty_width()
272             try:
273                 print_clean_line(dest, pending_items, width)
274             except (EnvironmentError, EOFError) as ex:
275                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
276     except BaseException as ex:
277         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
278     if pending_ex:
279         raise pending_ex
280
281
282 def import_and_run_main(module, args):
283     if do_profile:
284         import cProfile
285         f = compile('module.main(args)', __file__, 'exec')
286         cProfile.runctx(f, globals(), locals())
287     else:
288         module.main(args)
289
290
291 def run_module_cmd(module, args):
292     if not (fix_stdout or fix_stderr):
293         import_and_run_main(module, args)
294         return
295     # Interpose filter_output between all attempts to write to the
296     # stdout/stderr and the real stdout/stderr (e.g. the fds that
297     # connect directly to the terminal) via a thread that runs
298     # filter_output in a pipeline.
299     srcs = []
300     dests = []
301     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
302     filter_thread = filter_thread_started = None
303     pending_ex = None
304     try:
305         if fix_stdout:
306             sys.stdout.flush()
307             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
308             real_out_fd = os.dup(sys.stdout.fileno())
309             os.dup2(stdout_pipe[1], sys.stdout.fileno())
310             srcs.append(stdout_pipe[0])
311             dests.append(real_out_fd)
312         if fix_stderr:
313             sys.stderr.flush()
314             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
315             real_err_fd = os.dup(sys.stderr.fileno())
316             os.dup2(stderr_pipe[1], sys.stderr.fileno())
317             srcs.append(stderr_pipe[0])
318             dests.append(real_err_fd)
319
320         filter_thread = Thread(name='output filter',
321                                target=lambda : filter_output(srcs, dests))
322         filter_thread.start()
323         filter_thread_started = True
324         import_and_run_main(module, args)
325     except Exception as ex:
326         add_ex_tb(ex)
327         pending_ex = ex
328         raise
329     finally:
330         # Try to make sure that whatever else happens, we restore
331         # stdout and stderr here, if that's possible, so that we don't
332         # risk just losing some output.
333         try:
334             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
335         except Exception as ex:
336             add_ex_tb(ex)
337             add_ex_ctx(ex, pending_ex)
338         try:
339             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
340         except Exception as ex:
341             add_ex_tb(ex)
342             add_ex_ctx(ex, pending_ex)
343         # Kick filter loose
344         try:
345             stdout_pipe is not None and os.close(stdout_pipe[1])
346         except Exception as ex:
347             add_ex_tb(ex)
348             add_ex_ctx(ex, pending_ex)
349         try:
350             stderr_pipe is not None and os.close(stderr_pipe[1])
351         except Exception as ex:
352             add_ex_tb(ex)
353             add_ex_ctx(ex, pending_ex)
354         try:
355             close_catpipes()
356         except Exception as ex:
357             add_ex_tb(ex)
358             add_ex_ctx(ex, pending_ex)
359     if pending_ex:
360         raise pending_ex
361     # There's no point in trying to join unless we finished the finally block.
362     if filter_thread_started:
363         filter_thread.join()
364
365
366 def run_subproc_cmd(args):
367
368     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
369     if not (fix_stdout or fix_stderr):
370         os.execvp(c[0], c)
371
372     sys.stdout.flush()
373     sys.stderr.flush()
374     out = byte_stream(sys.stdout)
375     err = byte_stream(sys.stderr)
376     p = None
377     try:
378         p = subprocess.Popen(c,
379                              stdout=PIPE if fix_stdout else out,
380                              stderr=PIPE if fix_stderr else err,
381                              bufsize=4096, close_fds=True)
382         # Assume p will receive these signals and quit, which will
383         # then cause us to quit.
384         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
385             signal.signal(sig, signal.SIG_IGN)
386
387         srcs = []
388         dests = []
389         if fix_stdout:
390             srcs.append(p.stdout.fileno())
391             dests.append(out.fileno())
392         if fix_stderr:
393             srcs.append(p.stderr.fileno())
394             dests.append(err.fileno())
395         filter_output(srcs, dests)
396         return p.wait()
397     except BaseException as ex:
398         add_ex_tb(ex)
399         try:
400             if p and p.poll() == None:
401                 os.kill(p.pid, signal.SIGTERM)
402                 p.wait()
403         except BaseException as kill_ex:
404             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
405         raise ex
406
407
408 def run_subcmd(module, args):
409     if module:
410         run_module_cmd(module, args)
411     else:
412         run_subproc_cmd(args)
413
414 def main():
415     wrap_main(lambda : run_subcmd(cmd_module, subcmd))
416
417 if __name__ == "__main__":
418     main()