Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / commandline.py
1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Purpose of this module is to hold common script/commandline functionality.
6
7 This ranges from optparse, to a basic script wrapper setup (much like
8 what is used for chromite.bin.*).
9 """
10
11 from __future__ import print_function
12
13 import argparse
14 import collections
15 import datetime
16 import functools
17 import logging
18 import os
19 import optparse
20 import signal
21 import sys
22 import tempfile
23 import urlparse
24
25 # TODO(build): sort the cbuildbot.constants/lib.constants issue;
26 # lib shouldn't have to import from buildbot like this.
27 from chromite.cbuildbot import constants
28 from chromite.lib import cros_build_lib
29 from chromite.lib import git
30 from chromite.lib import gs
31 from chromite.lib import osutils
32 from chromite.lib import terminal
33
34
35 CHECKOUT_TYPE_UNKNOWN = 'unknown'
36 CHECKOUT_TYPE_GCLIENT = 'gclient'
37 CHECKOUT_TYPE_REPO = 'repo'
38 CHECKOUT_TYPE_SUBMODULE = 'submodule'
39
40
41 CheckoutInfo = collections.namedtuple(
42     'CheckoutInfo', ['type', 'root', 'chrome_src_dir'])
43
44
45 class ChrootRequiredError(Exception):
46   """Raised when a command must be run in the chroot
47
48   This exception is intended to be caught by code which will restart execution
49   in the chroot. If none of the arguments passed to the script need to be
50   adjusted when that happens, it can be constructed with no parameters. If
51   something does need to be adjusted, for instance an argument that's a path,
52   the command can construct a custom command line and pass it into this
53   exception which will be used instead.
54
55   When customizing the command line, argv[0] will have to be fixed up manually
56   like any other element of argv.
57   """
58
59   def __init__(self, new_argv=None, *args, **kwargs):
60     Exception.__init__(self, *args, **kwargs)
61     if new_argv is None:
62       new_argv = sys.argv[:]
63       new_argv = [git.ReinterpretPathForChroot(new_argv[0])] + new_argv[1:]
64
65     self.new_argv = new_argv
66
67
68 def DetermineCheckout(cwd):
69   """Gather information on the checkout we are in.
70
71   Returns:
72     A CheckoutInfo object with these attributes:
73       type: The type of checkout.  Valid values are CHECKOUT_TYPE_*.
74       root: The root of the checkout.
75       chrome_src_dir: If the checkout is a Chrome checkout, the path to the
76         Chrome src/ directory.
77   """
78   checkout_type = CHECKOUT_TYPE_UNKNOWN
79   root, path = None, None
80   for path in osutils.IteratePathParents(cwd):
81     repo_dir = os.path.join(path, '.repo')
82     if os.path.isdir(repo_dir):
83       checkout_type = CHECKOUT_TYPE_REPO
84       break
85     gclient_file = os.path.join(path, '.gclient')
86     if os.path.exists(gclient_file):
87       checkout_type = CHECKOUT_TYPE_GCLIENT
88       break
89     submodule_git = os.path.join(path, '.git')
90     if (os.path.isdir(submodule_git) and
91         git.IsSubmoduleCheckoutRoot(cwd, 'origin', constants.CHROMIUM_GOB_URL)):
92       checkout_type = CHECKOUT_TYPE_SUBMODULE
93       break
94
95   if checkout_type != CHECKOUT_TYPE_UNKNOWN:
96     root = path
97
98   # Determine the chrome src directory.
99   chrome_src_dir = None
100   if checkout_type == CHECKOUT_TYPE_GCLIENT:
101     chrome_src_dir = os.path.join(root, 'src')
102   elif checkout_type == CHECKOUT_TYPE_SUBMODULE:
103     chrome_src_dir = root
104
105   return CheckoutInfo(checkout_type, root, chrome_src_dir)
106
107
108 def GetCacheDir():
109   """Calculate the current cache dir.
110
111   Users can configure the cache dir using the --cache-dir argument and it is
112   shared between cbuildbot and all child processes. If no cache dir is
113   specified, FindCacheDir finds an alternative location to store the cache.
114
115   Returns:
116     The path to the cache dir.
117   """
118   return os.environ.get(
119       constants.SHARED_CACHE_ENVVAR,
120       BaseParser.FindCacheDir(None, None))
121
122
123 def AbsolutePath(_option, _opt, value):
124   """Expand paths and make them absolute."""
125   return osutils.ExpandPath(value)
126
127
128 def NormalizeGSPath(value):
129   """Normalize GS paths."""
130   url = gs.CanonicalizeURL(value, strict=True)
131   return '%s%s' % (gs.BASE_GS_URL, os.path.normpath(url[len(gs.BASE_GS_URL):]))
132
133
134 def NormalizeLocalOrGSPath(value):
135   """Normalize a local or GS path."""
136   ptype = 'gs_path' if value.startswith(gs.BASE_GS_URL) else 'path'
137   return VALID_TYPES[ptype](value)
138
139
140 def ParseDate(value):
141   """Parse date argument into a datetime.date object.
142
143   Args:
144     value: String representing a single date in "YYYY-MM-DD" format.
145
146   Returns:
147     A datetime.date object.
148   """
149   try:
150     return datetime.datetime.strptime(value, '%Y-%m-%d').date()
151   except ValueError:
152     # Give a helpful error message about the format expected.  Putting this
153     # message in the exception is useless because argparse ignores the
154     # exception message and just says the value is invalid.
155     cros_build_lib.Error('Date is expected to be in format YYYY-MM-DD.')
156     raise
157
158
159 def NormalizeUri(value):
160   """Normalize a local path or URI."""
161   # Pylint is confused about result of urlparse.
162   # pylint: disable=E1101
163   o = urlparse.urlparse(value)
164   if o.scheme == 'file':
165     # Trim off the file:// prefix.
166     return VALID_TYPES['path'](value[7:])
167   elif o.scheme not in ('', 'gs'):
168     o = list(o)
169     o[2] = os.path.normpath(o[2])
170     return urlparse.urlunparse(o)
171   else:
172     return NormalizeLocalOrGSPath(value)
173
174
175 def OptparseWrapCheck(desc, check_f, _option, opt, value):
176   """Optparse adapter for type checking functionality."""
177   try:
178     return check_f(value)
179   except ValueError:
180     raise optparse.OptionValueError(
181         'Invalid %s given: --%s=%s' % (desc, opt, value))
182
183
184 VALID_TYPES = {
185     'date': ParseDate,
186     'path': osutils.ExpandPath,
187     'gs_path': NormalizeGSPath,
188     'local_or_gs_path': NormalizeLocalOrGSPath,
189     'path_or_uri': NormalizeUri,
190 }
191
192
193 class Option(optparse.Option):
194   """Subclass to implement path evaluation & other useful types."""
195
196   _EXTRA_TYPES = ("path", "gs_path")
197   TYPES = optparse.Option.TYPES + _EXTRA_TYPES
198   TYPE_CHECKER = optparse.Option.TYPE_CHECKER.copy()
199   for t in _EXTRA_TYPES:
200     TYPE_CHECKER[t] = functools.partial(OptparseWrapCheck, t, VALID_TYPES[t])
201
202
203 class FilteringOption(Option):
204   """Subclass that supports Option filtering for FilteringOptionParser"""
205
206   def take_action(self, action, dest, opt, value, values, parser):
207     if action in FilteringOption.ACTIONS:
208       Option.take_action(self, action, dest, opt, value, values, parser)
209
210     if value is None:
211       value = []
212     elif not self.nargs or self.nargs <= 1:
213       value = [value]
214
215     parser.AddParsedArg(self, opt, [str(v) for v in value])
216
217
218 # TODO: logging.Formatter is not a subclass of object in python
219 # 2.6. Make ColoredFormatter explicitly inherit from object so that
220 # functions such as super() will not fail. This should be removed
221 # after python is upgraded to 2.7 on master2 (crbug.com/409273).
222 class ColoredFormatter(logging.Formatter, object):
223   """A logging formatter that can color the messages."""
224
225   _COLOR_MAPPING = {
226       'WARNING': terminal.Color.YELLOW,
227       'ERROR': terminal.Color.RED,
228   }
229
230   def __init__(self, *args, **kwargs):
231     """Initializes the formatter.
232
233     Args:
234       args: See logging.Formatter for specifics.
235       kwargs: See logging.Formatter for specifics.
236       enable_color: Whether to enable colored logging. Defaults
237         to None, where terminal.Color will set to a sane default.
238     """
239     self.color = terminal.Color(enabled=kwargs.pop('enable_color', None))
240     super(ColoredFormatter, self).__init__(*args, **kwargs)
241
242   def format(self, record, **kwargs):
243     """Formats |record| with color."""
244     msg = super(ColoredFormatter, self).format(record, **kwargs)
245     color = self._COLOR_MAPPING.get(record.levelname)
246     return msg if not color else self.color.Color(color, msg)
247
248
249 class ChromiteStreamHandler(logging.StreamHandler):
250   """A stream handler for logging."""
251
252
253 class BaseParser(object):
254   """Base parser class that includes the logic to add logging controls."""
255
256   DEFAULT_LOG_LEVELS = ('fatal', 'critical', 'error', 'warning', 'info',
257                         'debug')
258
259   DEFAULT_LOG_LEVEL = "info"
260   ALLOW_LOGGING = True
261
262   REPO_CACHE_DIR = '.cache'
263   CHROME_CACHE_DIR = '.cros_cache'
264
265   def __init__(self, **kwargs):
266     """Initialize this parser instance.
267
268     kwargs:
269       logging: Defaults to ALLOW_LOGGING from the class; if given,
270         add --log-level.
271       default_log_level: If logging is enabled, override the default logging
272         level.  Defaults to the class's DEFAULT_LOG_LEVEL value.
273       log_levels: If logging is enabled, this overrides the enumeration of
274         allowed logging levels.  If not given, defaults to the classes
275         DEFAULT_LOG_LEVELS value.
276       manual_debug: If logging is enabled and this is True, suppress addition
277         of a --debug alias.  This option defaults to True unless 'debug' has
278         been exempted from the allowed logging level targets.
279       caching: If given, must be either a callable that discerns the cache
280         location if it wasn't specified (the prototype must be akin to
281         lambda parser, values:calculated_cache_dir_path; it may return None to
282         indicate that it handles setting the value on its own later in the
283         parsing including setting the env), or True; if True, the
284         machinery defaults to invoking the class's FindCacheDir method
285         (which can be overridden).  FindCacheDir $CROS_CACHEDIR, falling
286         back to $REPO/.cache, finally falling back to $TMP.
287         Note that the cache_dir is not created, just discerned where it
288         should live.
289         If False, or caching is not given, then no --cache-dir option will be
290         added.
291     """
292     self.debug_enabled = False
293     self.caching_group = None
294     self.debug_group = None
295     self.default_log_level = None
296     self.log_levels = None
297     self.logging_enabled = kwargs.get('logging', self.ALLOW_LOGGING)
298     self.default_log_level = kwargs.get('default_log_level',
299                                         self.DEFAULT_LOG_LEVEL)
300     self.log_levels = tuple(x.lower() for x in
301                             kwargs.get('log_levels', self.DEFAULT_LOG_LEVELS))
302     self.debug_enabled = (not kwargs.get('manual_debug', False)
303                           and 'debug' in self.log_levels)
304     self.caching = kwargs.get('caching', False)
305
306   @staticmethod
307   def PopUsedArgs(kwarg_dict):
308     """Removes keys used by the base parser from the kwarg namespace."""
309     parser_keys = ['logging', 'default_log_level', 'log_levels', 'manual_debug',
310                    'caching']
311     for key in parser_keys:
312       kwarg_dict.pop(key, None)
313
314   def SetupOptions(self):
315     """Sets up special chromite options for an OptionParser."""
316     if self.logging_enabled:
317       self.debug_group = self.add_option_group("Debug options")
318       self.add_option_to_group(
319           self.debug_group, '--log-level', choices=self.log_levels,
320           default=self.default_log_level,
321           help='Set logging level to report at.')
322       self.add_option_to_group(
323           self.debug_group, '--log_format', action='store',
324           default=constants.LOGGER_FMT,
325           help='Set logging format to use.')
326       if self.debug_enabled:
327         self.add_option_to_group(
328           self.debug_group, '--debug', action='store_const', const='debug',
329           dest='log_level', help='Alias for `--log-level=debug`. '
330           'Useful for debugging bugs/failures.')
331       self.add_option_to_group(
332         self.debug_group, '--nocolor', action='store_false', dest='color',
333         default=None,
334         help='Do not use colorized output (or `export NOCOLOR=true`)')
335
336     if self.caching:
337       self.caching_group = self.add_option_group("Caching Options")
338       self.add_option_to_group(
339           self.caching_group, "--cache-dir", default=None, type='path',
340           help="Override the calculated chromeos cache directory; "
341           "typically defaults to '$REPO/.cache' .")
342
343   def SetupLogging(self, opts):
344     """Sets up logging based on |opts|."""
345     value = opts.log_level.upper()
346     logger = logging.getLogger()
347     logger.setLevel(getattr(logging, value))
348     formatter = ColoredFormatter(fmt=opts.log_format,
349                                  datefmt=constants.LOGGER_DATE_FMT,
350                                  enable_color=opts.color)
351
352     # Only set colored formatter for ChromiteStreamHandler instances,
353     # which could have been added by ScriptWrapperMain() below.
354     chromite_handlers = [x for x in logger.handlers if
355                          isinstance(x, ChromiteStreamHandler)]
356     for handler in chromite_handlers:
357       handler.setFormatter(formatter)
358
359     return value
360
361   def DoPostParseSetup(self, opts, args):
362     """Method called to handle post opts/args setup.
363
364     This can be anything from logging setup to positional arg count validation.
365
366     Args:
367       opts: optparse.Values or argparse.Namespace instance
368       args: position arguments unconsumed from parsing.
369
370     Returns:
371       (opts, args), w/ whatever modification done.
372     """
373     if self.logging_enabled:
374       value = self.SetupLogging(opts)
375       if self.debug_enabled:
376         opts.debug = (value == "DEBUG")
377
378     if self.caching:
379       path = os.environ.get(constants.SHARED_CACHE_ENVVAR)
380       if path is not None and opts.cache_dir is None:
381         opts.cache_dir = os.path.abspath(path)
382
383       opts.cache_dir_specified = opts.cache_dir is not None
384       if not opts.cache_dir_specified:
385         func = self.FindCacheDir if not callable(self.caching) else self.caching
386         opts.cache_dir = func(self, opts)
387       if opts.cache_dir is not None:
388         self.ConfigureCacheDir(opts.cache_dir)
389
390     return opts, args
391
392   @staticmethod
393   def ConfigureCacheDir(cache_dir):
394     if cache_dir is None:
395       os.environ.pop(constants.SHARED_CACHE_ENVVAR, None)
396       logging.debug("Removed cache_dir setting")
397     else:
398       os.environ[constants.SHARED_CACHE_ENVVAR] = cache_dir
399       logging.debug("Configured cache_dir to %r", cache_dir)
400
401   @classmethod
402   def FindCacheDir(cls, _parser, _opts):
403     logging.debug('Cache dir lookup.')
404     checkout = DetermineCheckout(os.getcwd())
405     path = None
406     if checkout.type == CHECKOUT_TYPE_REPO:
407       path = os.path.join(checkout.root, cls.REPO_CACHE_DIR)
408     elif checkout.type in (CHECKOUT_TYPE_GCLIENT, CHECKOUT_TYPE_SUBMODULE):
409       path = os.path.join(checkout.root, cls.CHROME_CACHE_DIR)
410     elif checkout.type == CHECKOUT_TYPE_UNKNOWN:
411       path = os.path.join(tempfile.gettempdir(), 'chromeos-cache')
412     else:
413       raise AssertionError('Unexpected type %s' % checkout.type)
414
415     return path
416
417   def add_option_group(self, *args, **kwargs):
418     """Returns a new option group see optparse.OptionParser.add_option_group."""
419     raise NotImplementedError('Subclass must override this method')
420
421   @staticmethod
422   def add_option_to_group(group, *args, **kwargs):
423     """Adds the given option defined by args and kwargs to group."""
424     group.add_option(*args, **kwargs)
425
426
427 class ArgumentNamespace(argparse.Namespace):
428   """Class to mimic argparse.Namespace with value freezing support."""
429   __metaclass__ = cros_build_lib.FrozenAttributesClass
430   _FROZEN_ERR_MSG = 'Option values are frozen, cannot alter %s.'
431
432
433 # Note that because optparse.Values is not a new-style class this class
434 # must use the mixin FrozenAttributesMixin rather than the metaclass
435 # FrozenAttributesClass.
436 class OptionValues(cros_build_lib.FrozenAttributesMixin, optparse.Values):
437   """Class to mimic optparse.Values with value freezing support."""
438   _FROZEN_ERR_MSG = 'Option values are frozen, cannot alter %s.'
439
440   def __init__(self, defaults, *args, **kwargs):
441     cros_build_lib.FrozenAttributesMixin.__init__(self)
442     optparse.Values.__init__(self, defaults, *args, **kwargs)
443
444     # Used by FilteringParser.
445     self.parsed_args = None
446
447
448 class OptionParser(optparse.OptionParser, BaseParser):
449   """Custom parser adding our custom option class in.
450
451   Aside from adding a couple of types (path for absolute paths,
452   gs_path for google storage urls, and log_level for logging level control),
453   this additionally exposes logging control by default; if undesired,
454   either derive from this class setting ALLOW_LOGGING to False, or
455   pass in logging=False to the constructor.
456   """
457
458   DEFAULT_OPTION_CLASS = Option
459
460   def __init__(self, usage=None, **kwargs):
461     BaseParser.__init__(self, **kwargs)
462     self.PopUsedArgs(kwargs)
463     kwargs.setdefault("option_class", self.DEFAULT_OPTION_CLASS)
464     optparse.OptionParser.__init__(self, usage=usage, **kwargs)
465     self.SetupOptions()
466
467   def parse_args(self, args=None, values=None):
468     # If no Values object is specified then use our custom OptionValues.
469     if values is None:
470       values = OptionValues(defaults=self.defaults)
471
472     opts, remaining = optparse.OptionParser.parse_args(
473         self, args=args, values=values)
474     return self.DoPostParseSetup(opts, remaining)
475
476
477 PassedOption = collections.namedtuple(
478         'PassedOption', ['opt_inst', 'opt_str', 'value_str'])
479
480
481 class FilteringParser(OptionParser):
482   """Custom option parser for filtering options."""
483
484   DEFAULT_OPTION_CLASS = FilteringOption
485
486   def parse_args(self, args=None, values=None):
487     # If no Values object is specified then use our custom OptionValues.
488     if values is None:
489       values = OptionValues(defaults=self.defaults)
490
491     values.parsed_args = []
492
493     return OptionParser.parse_args(self, args=args, values=values)
494
495   def AddParsedArg(self, opt_inst, opt_str, value_str):
496     """Add a parsed argument with attributes.
497
498     Args:
499       opt_inst: An instance of a raw optparse.Option object that represents the
500                 option.
501       opt_str: The option string.
502       value_str: A list of string-ified values dentified by OptParse.
503     """
504     self.values.parsed_args.append(PassedOption(opt_inst, opt_str, value_str))
505
506   @staticmethod
507   def FilterArgs(parsed_args, filter_fn):
508     """Filter the argument by passing it through a function.
509
510     Args:
511       parsed_args: The list of parsed argument namedtuples to filter.  Tuples
512         are of the form (opt_inst, opt_str, value_str).
513       filter_fn: A function with signature f(PassedOption), and returns True if
514         the argument is to be passed through.  False if not.
515
516     Returns:
517       A tuple containing two lists - one of accepted arguments and one of
518       removed arguments.
519     """
520     removed = []
521     accepted = []
522     for arg in parsed_args:
523       target = accepted if filter_fn(arg) else removed
524       target.append(arg.opt_str)
525       target.extend(arg.value_str)
526
527     return accepted, removed
528
529
530 # pylint: disable=R0901
531 class ArgumentParser(BaseParser, argparse.ArgumentParser):
532   """Custom argument parser for use by chromite.
533
534   This class additionally exposes logging control by default; if undesired,
535   either derive from this class setting ALLOW_LOGGING to False, or
536   pass in logging=False to the constructor.
537   """
538   # pylint: disable=W0231
539   def __init__(self, usage=None, **kwargs):
540     kwargs.setdefault('formatter_class', argparse.RawDescriptionHelpFormatter)
541     BaseParser.__init__(self, **kwargs)
542     self.PopUsedArgs(kwargs)
543     argparse.ArgumentParser.__init__(self, usage=usage, **kwargs)
544     self._SetupTypes()
545     self.SetupOptions()
546
547   def _SetupTypes(self):
548     """Register types with ArgumentParser."""
549     for t, check_f in VALID_TYPES.iteritems():
550       self.register('type', t, check_f)
551
552   def add_option_group(self, *args, **kwargs):
553     """Return an argument group rather than an option group."""
554     return self.add_argument_group(*args, **kwargs)
555
556   @staticmethod
557   def add_option_to_group(group, *args, **kwargs):
558     """Adds an argument rather than an option to the given group."""
559     return group.add_argument(*args, **kwargs)
560
561   def parse_args(self, args=None, namespace=None):
562     """Translates OptionParser call to equivalent ArgumentParser call."""
563     # If no Namespace object is specified then use our custom ArgumentNamespace.
564     if namespace is None:
565       namespace = ArgumentNamespace()
566
567     # Unlike OptionParser, ArgParser works only with a single namespace and no
568     # args. Re-use BaseParser DoPostParseSetup but only take the namespace.
569     namespace = argparse.ArgumentParser.parse_args(
570         self, args=args, namespace=namespace)
571     return self.DoPostParseSetup(namespace, None)[0]
572
573
574 class _ShutDownException(SystemExit):
575   """Exception raised when user hits CTRL+C."""
576
577   def __init__(self, sig_num, message):
578     self.signal = sig_num
579     # Setup a usage message primarily for any code that may intercept it
580     # while this exception is crashing back up the stack to us.
581     SystemExit.__init__(self, message)
582     self.args = (sig_num, message)
583
584
585 def _DefaultHandler(signum, _frame):
586   # Don't double process sigterms; just trigger shutdown from the first
587   # exception.
588   signal.signal(signum, signal.SIG_IGN)
589   raise _ShutDownException(
590       signum, "Received signal %i; shutting down" % (signum,))
591
592
593 def _RestartInChroot(argv):
594   """Rerun the current command inside the chroot"""
595   return cros_build_lib.RunCommand(argv, enter_chroot=True, error_code_ok=True,
596                                    cwd=constants.SOURCE_ROOT).returncode
597
598
599 def ScriptWrapperMain(find_target_func, argv=None,
600                       log_level=logging.DEBUG,
601                       log_format=constants.LOGGER_FMT):
602   """Function usable for chromite.script.* style wrapping.
603
604   Note that this function invokes sys.exit on the way out by default.
605
606   Args:
607     find_target_func: a function, which, when given the absolute
608       pathway the script was invoked via (for example,
609       /home/ferringb/cros/trunk/chromite/bin/cros_sdk; note that any
610       trailing .py from the path name will be removed),
611       will return the main function to invoke (that functor will take
612       a single arg- a list of arguments, and shall return either None
613       or an integer, to indicate the exit code).
614     argv: sys.argv, or an equivalent tuple for testing.  If nothing is
615       given, sys.argv is defaulted to.
616     log_level: Default logging level to start at.
617     log_format: Default logging format to use.
618   """
619   if argv is None:
620     argv = sys.argv[:]
621   target = os.path.abspath(argv[0])
622   name = os.path.basename(target)
623   if target.endswith('.py'):
624     target = os.path.splitext(target)[0]
625   target = find_target_func(target)
626   if target is None:
627     print('Internal error detected- no main functor found in module %r.' %
628           (name,), file=sys.stderr)
629     sys.exit(100)
630
631   # Set up basic logging information for all modules that use logging.
632   # Note a script target may setup default logging in its module namespace
633   # which will take precedence over this.
634   logger = logging.getLogger()
635   logger.setLevel(log_level)
636   logger_handler = ChromiteStreamHandler()
637   logger_handler.setFormatter(
638       logging.Formatter(fmt=log_format, datefmt=constants.LOGGER_DATE_FMT))
639   logger.addHandler(logger_handler)
640
641   signal.signal(signal.SIGTERM, _DefaultHandler)
642
643   ret = 1
644   try:
645     ret = target(argv[1:])
646   except _ShutDownException as e:
647     sys.stdout.flush()
648     print('%s: Signaled to shutdown: caught %i signal.' % (name, e.signal,),
649           file=sys.stderr)
650     sys.stderr.flush()
651   except SystemExit as e:
652     # Right now, let this crash through- longer term, we'll update the scripts
653     # in question to not use sys.exit, and make this into a flagged error.
654     raise
655   except ChrootRequiredError as e:
656     ret = _RestartInChroot(e.new_argv)
657   except Exception as e:
658     sys.stdout.flush()
659     print('%s: Unhandled exception:' % (name,), file=sys.stderr)
660     sys.stderr.flush()
661     raise
662   finally:
663     logging.shutdown()
664
665   if ret is None:
666     ret = 0
667   sys.exit(ret)