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