Kuang-che Wu | 6e4beca | 2018-06-27 17:45:02 +0800 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 2 | # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
Kuang-che Wu | 2526a67 | 2019-09-10 16:23:59 +0800 | [diff] [blame] | 5 | """Command line utility functions.""" |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 6 | |
| 7 | from __future__ import print_function |
| 8 | import argparse |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 9 | import logging |
| 10 | import os |
| 11 | import re |
Kuang-che Wu | 443633f | 2019-02-27 00:58:33 +0800 | [diff] [blame] | 12 | import signal |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 13 | import sys |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 14 | |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 15 | from bisect_kit import errors |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 16 | from bisect_kit import util |
| 17 | |
| 18 | logger = logging.getLogger(__name__) |
| 19 | |
Kuang-che Wu | 0476d1f | 2019-03-04 19:27:01 +0800 | [diff] [blame] | 20 | # Exit code of bisect eval script. These values are chosen compatible with 'git |
| 21 | # bisect'. |
| 22 | EXIT_CODE_OLD = 0 |
| 23 | EXIT_CODE_NEW = 1 |
| 24 | EXIT_CODE_SKIP = 125 |
| 25 | EXIT_CODE_FATAL = 128 |
| 26 | |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 27 | |
| 28 | class ArgTypeError(argparse.ArgumentTypeError): |
| 29 | """An error for argument validation failure. |
| 30 | |
| 31 | This not only tells users the argument is wrong but also gives correct |
| 32 | example. The main purpose of this error is for argtype_multiplexer, which |
| 33 | cascades examples from multiple ArgTypeError. |
| 34 | """ |
| 35 | |
| 36 | def __init__(self, msg, example): |
| 37 | self.msg = msg |
| 38 | if isinstance(example, list): |
| 39 | self.example = example |
| 40 | else: |
| 41 | self.example = [example] |
| 42 | full_msg = '%s (example value: %s)' % (self.msg, ', '.join(self.example)) |
| 43 | super(ArgTypeError, self).__init__(full_msg) |
| 44 | |
| 45 | |
| 46 | def argtype_notempty(s): |
| 47 | """Validates argument is not an empty string. |
| 48 | |
| 49 | Args: |
| 50 | s: string to validate. |
| 51 | |
| 52 | Raises: |
| 53 | ArgTypeError if argument is empty string. |
| 54 | """ |
| 55 | if not s: |
| 56 | msg = 'should not be empty' |
| 57 | raise ArgTypeError(msg, 'foo') |
| 58 | return s |
| 59 | |
| 60 | |
| 61 | def argtype_int(s): |
| 62 | """Validate argument is a number. |
| 63 | |
| 64 | Args: |
| 65 | s: string to validate. |
| 66 | |
| 67 | Raises: |
| 68 | ArgTypeError if argument is not a number. |
| 69 | """ |
| 70 | try: |
| 71 | return str(int(s)) |
| 72 | except ValueError: |
| 73 | raise ArgTypeError('should be a number', '123') |
| 74 | |
| 75 | |
Kuang-che Wu | 603cdad | 2019-01-18 21:32:55 +0800 | [diff] [blame] | 76 | def argtype_re(pattern, example): |
| 77 | r"""Validate argument matches `pattern`. |
| 78 | |
| 79 | Args: |
| 80 | pattern: regex pattern |
| 81 | example: example string which matches `pattern` |
| 82 | |
| 83 | Returns: |
| 84 | A new argtype function which matches regex `pattern` |
| 85 | """ |
| 86 | assert re.match(pattern, example) |
| 87 | |
| 88 | def validate(s): |
| 89 | if re.match(pattern, s): |
| 90 | return s |
| 91 | if re.escape(pattern) == pattern: |
| 92 | raise ArgTypeError('should be "%s"' % pattern, pattern) |
| 93 | raise ArgTypeError('should match "%s"' % pattern, |
| 94 | '"%s" like %s' % (pattern, example)) |
| 95 | |
| 96 | return validate |
| 97 | |
| 98 | |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 99 | def argtype_multiplexer(*args): |
| 100 | r"""argtype multiplexer |
| 101 | |
Kuang-che Wu | 603cdad | 2019-01-18 21:32:55 +0800 | [diff] [blame] | 102 | This function takes a list of argtypes and creates a new function matching |
| 103 | them. Moreover, it gives error message with examples. |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 104 | |
Kuang-che Wu | baaa453 | 2018-08-15 17:08:10 +0800 | [diff] [blame] | 105 | Examples: |
Kuang-che Wu | 603cdad | 2019-01-18 21:32:55 +0800 | [diff] [blame] | 106 | >>> argtype = argtype_multiplexer(argtype_int, |
| 107 | argtype_re(r'^r\d+$', 'r123')) |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 108 | >>> argtype('123') |
| 109 | 123 |
| 110 | >>> argtype('r456') |
| 111 | r456 |
| 112 | >>> argtype('hello') |
Kuang-che Wu | 603cdad | 2019-01-18 21:32:55 +0800 | [diff] [blame] | 113 | ArgTypeError: Invalid argument (example value: 123, "r\d+$" like r123) |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 114 | |
| 115 | Args: |
| 116 | *args: list of argtypes or regex pattern. |
| 117 | |
| 118 | Returns: |
| 119 | A new argtype function which matches *args. |
| 120 | """ |
| 121 | |
| 122 | def validate(s): |
| 123 | examples = [] |
| 124 | for t in args: |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 125 | try: |
| 126 | return t(s) |
| 127 | except ArgTypeError as e: |
| 128 | examples += e.example |
| 129 | |
| 130 | msg = 'Invalid argument' |
| 131 | raise ArgTypeError(msg, examples) |
| 132 | |
| 133 | return validate |
| 134 | |
| 135 | |
| 136 | def argtype_multiplier(argtype): |
| 137 | """A new argtype that supports multiplier suffix of the given argtype. |
| 138 | |
Kuang-che Wu | baaa453 | 2018-08-15 17:08:10 +0800 | [diff] [blame] | 139 | Examples: |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 140 | Supports the given argtype accepting "foo" as argument, this function |
| 141 | generates a new argtype function which accepts argument like "foo*3". |
| 142 | |
| 143 | Returns: |
| 144 | A new argtype function which returns (arg, times) where arg is accepted |
| 145 | by input `argtype` and times is repeating count. Note that if multiplier is |
| 146 | omitted, "times" is 1. |
| 147 | """ |
| 148 | |
| 149 | def helper(s): |
| 150 | m = re.match(r'^(.+)\*(\d+)$', s) |
| 151 | try: |
| 152 | if m: |
| 153 | return argtype(m.group(1)), int(m.group(2)) |
Kuang-che Wu | 68db08a | 2018-03-30 11:50:34 +0800 | [diff] [blame] | 154 | return argtype(s), 1 |
Kuang-che Wu | 88875db | 2017-07-20 10:47:53 +0800 | [diff] [blame] | 155 | except ArgTypeError as e: |
| 156 | # It should be okay to gives multiplier example only for the first one |
| 157 | # because it is just "example", no need to enumerate all possibilities. |
| 158 | raise ArgTypeError(e.msg, e.example + [e.example[0] + '*3']) |
| 159 | |
| 160 | return helper |
| 161 | |
| 162 | |
| 163 | def argtype_dir_path(s): |
| 164 | """Validate argument is an existing directory. |
| 165 | |
| 166 | Args: |
| 167 | s: string to validate. |
| 168 | |
| 169 | Raises: |
| 170 | ArgTypeError if the path is not a directory. |
| 171 | """ |
| 172 | if not os.path.exists(s): |
| 173 | raise ArgTypeError('should be an existing directory', '/path/to/somewhere') |
| 174 | if not os.path.isdir(s): |
| 175 | raise ArgTypeError('should be a directory', '/path/to/somewhere') |
| 176 | |
| 177 | # Normalize, trim trailing path separators. |
| 178 | if len(s) > 1 and s[-1] == os.path.sep: |
| 179 | s = s[:-1] |
| 180 | return s |
| 181 | |
| 182 | |
Kuang-che Wu | 8851888 | 2017-09-22 16:57:25 +0800 | [diff] [blame] | 183 | def check_executable(program): |
| 184 | """Checks whether a program is executable. |
| 185 | |
| 186 | Args: |
| 187 | program: program path in question |
| 188 | |
| 189 | Returns: |
| 190 | string as error message if `program` is not executable, or None otherwise. |
| 191 | It will return None if unable to determine as well. |
| 192 | """ |
| 193 | returncode = util.call('which', program) |
| 194 | if returncode == 127: # No 'which' on this platform, skip the check. |
| 195 | return None |
| 196 | if returncode == 0: # is executable |
| 197 | return None |
| 198 | |
| 199 | hint = '' |
| 200 | if not os.path.exists(program): |
| 201 | hint = 'Not in PATH?' |
| 202 | elif not os.path.isfile(program): |
| 203 | hint = 'Not a file' |
| 204 | elif not os.access(program, os.X_OK): |
| 205 | hint = 'Forgot to chmod +x?' |
| 206 | elif '/' not in program: |
| 207 | hint = 'Forgot to prepend "./" ?' |
| 208 | return '%r is not executable. %s' % (program, hint) |
| 209 | |
| 210 | |
Kuang-che Wu | bc7bfce | 2019-05-21 18:43:16 +0800 | [diff] [blame] | 211 | def lookup_signal_name(signum): |
| 212 | """Look up signal name by signal number. |
| 213 | |
| 214 | Args: |
| 215 | signum: signal number |
| 216 | |
| 217 | Returns: |
| 218 | signal name, like "SIGTERM". "Unknown" for unexpected number. |
| 219 | """ |
| 220 | for k, v in vars(signal).items(): |
| 221 | if k.startswith('SIG') and '_' not in k and v == signum: |
| 222 | return k |
| 223 | return 'Unknown' |
| 224 | |
| 225 | |
Kuang-che Wu | 443633f | 2019-02-27 00:58:33 +0800 | [diff] [blame] | 226 | def format_returncode(returncode): |
Kuang-che Wu | bc7bfce | 2019-05-21 18:43:16 +0800 | [diff] [blame] | 227 | # returncode is negative if the process is terminated by signal directly. |
Kuang-che Wu | 443633f | 2019-02-27 00:58:33 +0800 | [diff] [blame] | 228 | if returncode < 0: |
| 229 | signum = -returncode |
Kuang-che Wu | bc7bfce | 2019-05-21 18:43:16 +0800 | [diff] [blame] | 230 | signame = lookup_signal_name(signum) |
Kuang-che Wu | 443633f | 2019-02-27 00:58:33 +0800 | [diff] [blame] | 231 | return 'terminated by signal %d (%s)' % (signum, signame) |
Kuang-che Wu | bc7bfce | 2019-05-21 18:43:16 +0800 | [diff] [blame] | 232 | # Some programs, e.g. shell, handled signal X and exited with (128 + X). |
| 233 | if returncode > 128: |
| 234 | signum = returncode - 128 |
| 235 | signame = lookup_signal_name(signum) |
| 236 | return 'exited with code %d; may be signal %d (%s)' % (returncode, signum, |
| 237 | signame) |
Kuang-che Wu | 443633f | 2019-02-27 00:58:33 +0800 | [diff] [blame] | 238 | |
| 239 | return 'exited with code %d' % returncode |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 240 | |
| 241 | |
| 242 | def patching_argparser_exit(parser): |
| 243 | """Patching argparse.ArgumentParser.exit to exit with fatal exit code. |
| 244 | |
| 245 | Args: |
| 246 | parser: argparse.ArgumentParser object |
| 247 | """ |
| 248 | orig_exit = parser.exit |
| 249 | |
| 250 | def exit_hack(status=0, message=None): |
| 251 | if status != 0: |
| 252 | status = EXIT_CODE_FATAL |
| 253 | orig_exit(status, message) |
| 254 | |
| 255 | parser.exit = exit_hack |
| 256 | |
| 257 | |
| 258 | def fatal_error_handler(func): |
| 259 | """Function decorator which exits with fatal code for fatal exceptions. |
| 260 | |
| 261 | This is a helper for switcher and evaluator. It catches fatal exceptions and |
| 262 | exits with fatal exit code. The fatal exit code (128) is aligned with 'git |
| 263 | bisect'. |
| 264 | |
| 265 | See also argparse_fatal_error(). |
| 266 | |
| 267 | Args: |
| 268 | func: wrapped function |
| 269 | """ |
| 270 | |
| 271 | def wrapper(*args, **kwargs): |
| 272 | try: |
| 273 | return func(*args, **kwargs) |
| 274 | except (AssertionError, errors.ExecutionFatalError, errors.ArgumentError): |
Kuang-che Wu | 6157f1a | 2019-09-11 21:29:42 +0800 | [diff] [blame] | 275 | logger.exception('fatal exception, bisection should stop') |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 276 | sys.exit(EXIT_CODE_FATAL) |
| 277 | |
| 278 | return wrapper |