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