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