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