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