]> arthur.barton.de Git - bup.git/blob - lib/bup/main.py
main: fix output string truncation
[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     already_fixed = True
196 fix_stdout = not already_fixed and os.isatty(1)
197 fix_stderr = not already_fixed 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_types) 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()