]> arthur.barton.de Git - bup.git/blob - lib/bup/main.py
0c21c88f57037d48fa1a35e7c25af723bbafe7c4
[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(isinstance(x, int_types) for x in srcs)
242     assert len(srcs) == len(dests)
243     srcs = tuple(srcs)
244     dest_for = dict(zip(srcs, dests))
245     pending = {}
246     pending_ex = None
247     try:
248         while srcs:
249             ready_fds, _, _ = select.select(srcs, [], [])
250             width = tty_width()
251             for fd in ready_fds:
252                 buf = os.read(fd, 4096)
253                 dest = dest_for[fd]
254                 if not buf:
255                     srcs = tuple([x for x in srcs if x is not fd])
256                     print_clean_line(dest, pending.pop(fd, []), width)
257                 else:
258                     split = sep_rx.split(buf)
259                     while len(split) > 1:
260                         content, sep = split[:2]
261                         split = split[2:]
262                         print_clean_line(dest,
263                                          pending.pop(fd, []) + [content],
264                                          width,
265                                          sep)
266                     assert len(split) == 1
267                     if split[0]:
268                         pending.setdefault(fd, []).extend(split)
269     except BaseException as ex:
270         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
271     try:
272         # Try to finish each of the streams
273         for fd, pending_items in compat.items(pending):
274             dest = dest_for[fd]
275             width = tty_width()
276             try:
277                 print_clean_line(dest, pending_items, width)
278             except (EnvironmentError, EOFError) as ex:
279                 pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
280     except BaseException as ex:
281         pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex)
282     if pending_ex:
283         raise pending_ex
284
285
286 def import_and_run_main(module, args):
287     if do_profile:
288         import cProfile
289         f = compile('module.main(args)', __file__, 'exec')
290         cProfile.runctx(f, globals(), locals())
291     else:
292         module.main(args)
293
294
295 def run_module_cmd(module, args):
296     if not (fix_stdout or fix_stderr):
297         import_and_run_main(module, args)
298         return
299     # Interpose filter_output between all attempts to write to the
300     # stdout/stderr and the real stdout/stderr (e.g. the fds that
301     # connect directly to the terminal) via a thread that runs
302     # filter_output in a pipeline.
303     srcs = []
304     dests = []
305     real_out_fd = real_err_fd = stdout_pipe = stderr_pipe = None
306     filter_thread = filter_thread_started = None
307     pending_ex = None
308     try:
309         if fix_stdout:
310             sys.stdout.flush()
311             stdout_pipe = os.pipe()  # monitored_by_filter, stdout_everyone_uses
312             real_out_fd = os.dup(sys.stdout.fileno())
313             os.dup2(stdout_pipe[1], sys.stdout.fileno())
314             srcs.append(stdout_pipe[0])
315             dests.append(real_out_fd)
316         if fix_stderr:
317             sys.stderr.flush()
318             stderr_pipe = os.pipe()  # monitored_by_filter, stderr_everyone_uses
319             real_err_fd = os.dup(sys.stderr.fileno())
320             os.dup2(stderr_pipe[1], sys.stderr.fileno())
321             srcs.append(stderr_pipe[0])
322             dests.append(real_err_fd)
323
324         filter_thread = Thread(name='output filter',
325                                target=lambda : filter_output(srcs, dests))
326         filter_thread.start()
327         filter_thread_started = True
328         import_and_run_main(module, args)
329     except Exception as ex:
330         add_ex_tb(ex)
331         pending_ex = ex
332         raise
333     finally:
334         # Try to make sure that whatever else happens, we restore
335         # stdout and stderr here, if that's possible, so that we don't
336         # risk just losing some output.
337         try:
338             real_out_fd is not None and os.dup2(real_out_fd, sys.stdout.fileno())
339         except Exception as ex:
340             add_ex_tb(ex)
341             add_ex_ctx(ex, pending_ex)
342         try:
343             real_err_fd is not None and os.dup2(real_err_fd, sys.stderr.fileno())
344         except Exception as ex:
345             add_ex_tb(ex)
346             add_ex_ctx(ex, pending_ex)
347         # Kick filter loose
348         try:
349             stdout_pipe is not None and os.close(stdout_pipe[1])
350         except Exception as ex:
351             add_ex_tb(ex)
352             add_ex_ctx(ex, pending_ex)
353         try:
354             stderr_pipe is not None and os.close(stderr_pipe[1])
355         except Exception as ex:
356             add_ex_tb(ex)
357             add_ex_ctx(ex, pending_ex)
358         try:
359             close_catpipes()
360         except Exception as ex:
361             add_ex_tb(ex)
362             add_ex_ctx(ex, pending_ex)
363     if pending_ex:
364         raise pending_ex
365     # There's no point in trying to join unless we finished the finally block.
366     if filter_thread_started:
367         filter_thread.join()
368
369
370 def run_subproc_cmd(args):
371
372     c = (do_profile and [sys.executable, b'-m', b'cProfile'] or []) + args
373     if not (fix_stdout or fix_stderr):
374         os.execvp(c[0], c)
375
376     sys.stdout.flush()
377     sys.stderr.flush()
378     out = byte_stream(sys.stdout)
379     err = byte_stream(sys.stderr)
380     p = None
381     try:
382         p = subprocess.Popen(c,
383                              stdout=PIPE if fix_stdout else out,
384                              stderr=PIPE if fix_stderr else err,
385                              env=tty_env, bufsize=4096, close_fds=True)
386         # Assume p will receive these signals and quit, which will
387         # then cause us to quit.
388         for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
389             signal.signal(sig, signal.SIG_IGN)
390
391         srcs = []
392         dests = []
393         if fix_stdout:
394             srcs.append(p.stdout.fileno())
395             dests.append(out.fileno())
396         if fix_stderr:
397             srcs.append(p.stderr.fileno())
398             dests.append(err.fileno())
399         filter_output(srcs, dests)
400         return p.wait()
401     except BaseException as ex:
402         add_ex_tb(ex)
403         try:
404             if p and p.poll() == None:
405                 os.kill(p.pid, signal.SIGTERM)
406                 p.wait()
407         except BaseException as kill_ex:
408             raise add_ex_ctx(add_ex_tb(kill_ex), ex)
409         raise ex
410
411
412 def run_subcmd(module, args):
413     if module:
414         run_module_cmd(module, args)
415     else:
416         run_subproc_cmd(args)
417
418 def main():
419     wrap_main(lambda : run_subcmd(cmd_module, subcmd))
420
421 if __name__ == "__main__":
422     main()