kjellander | 0393de4 | 2017-06-18 13:21:21 -0700 | [diff] [blame] | 1 | # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
| 2 | # |
| 3 | # Use of this source code is governed by a BSD-style license |
| 4 | # that can be found in the LICENSE file in the root of the source |
| 5 | # tree. An additional intellectual property rights grant can be found |
| 6 | # in the file PATENTS. All contributing project authors may |
| 7 | # be found in the AUTHORS file in the root of the source tree. |
| 8 | |
| 9 | import logging |
| 10 | import platform |
| 11 | import os |
| 12 | import signal |
| 13 | import subprocess |
| 14 | import sys |
| 15 | import time |
| 16 | |
| 17 | |
| 18 | class NotImplementedError(Exception): |
| 19 | pass |
| 20 | |
| 21 | |
| 22 | class TimeoutError(Exception): |
| 23 | pass |
| 24 | |
| 25 | |
| 26 | def RunSubprocessInBackground(proc): |
| 27 | """Runs a subprocess in the background. Returns a handle to the process.""" |
| 28 | logging.info("running %s in the background" % " ".join(proc)) |
| 29 | return subprocess.Popen(proc) |
| 30 | |
| 31 | |
| 32 | def RunSubprocess(proc, timeout=0): |
| 33 | """ Runs a subprocess, until it finishes or |timeout| is exceeded and the |
| 34 | process is killed with taskkill. A |timeout| <= 0 means no timeout. |
| 35 | |
| 36 | Args: |
| 37 | proc: list of process components (exe + args) |
| 38 | timeout: how long to wait before killing, <= 0 means wait forever |
| 39 | """ |
| 40 | |
| 41 | logging.info("running %s, timeout %d sec" % (" ".join(proc), timeout)) |
| 42 | sys.stdout.flush() |
| 43 | sys.stderr.flush() |
| 44 | |
| 45 | # Manually read and print out stdout and stderr. |
| 46 | # By default, the subprocess is supposed to inherit these from its parent, |
| 47 | # however when run under buildbot, it seems unable to read data from a |
| 48 | # grandchild process, so we have to read the child and print the data as if |
| 49 | # it came from us for buildbot to read it. We're not sure why this is |
| 50 | # necessary. |
| 51 | # TODO(erikkay): should we buffer stderr and stdout separately? |
| 52 | p = subprocess.Popen(proc, universal_newlines=True, |
| 53 | bufsize=0, # unbuffered |
| 54 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 55 | |
| 56 | logging.info("started subprocess") |
| 57 | |
| 58 | did_timeout = False |
| 59 | if timeout > 0: |
| 60 | wait_until = time.time() + timeout |
| 61 | while p.poll() is None and not did_timeout: |
| 62 | # Have to use readline rather than readlines() or "for line in p.stdout:", |
| 63 | # otherwise we get buffered even with bufsize=0. |
| 64 | line = p.stdout.readline() |
| 65 | while line and not did_timeout: |
| 66 | sys.stdout.write(line) |
| 67 | sys.stdout.flush() |
| 68 | line = p.stdout.readline() |
| 69 | if timeout > 0: |
| 70 | did_timeout = time.time() > wait_until |
| 71 | |
| 72 | if did_timeout: |
| 73 | logging.info("process timed out") |
| 74 | else: |
| 75 | logging.info("process ended, did not time out") |
| 76 | |
| 77 | if did_timeout: |
| 78 | if IsWindows(): |
| 79 | subprocess.call(["taskkill", "/T", "/F", "/PID", str(p.pid)]) |
| 80 | else: |
| 81 | # Does this kill all children, too? |
| 82 | os.kill(p.pid, signal.SIGINT) |
| 83 | logging.error("KILLED %d" % p.pid) |
| 84 | # Give the process a chance to actually die before continuing |
| 85 | # so that cleanup can happen safely. |
| 86 | time.sleep(1.0) |
| 87 | logging.error("TIMEOUT waiting for %s" % proc[0]) |
| 88 | raise TimeoutError(proc[0]) |
| 89 | else: |
| 90 | for line in p.stdout: |
| 91 | sys.stdout.write(line) |
| 92 | if not IsMac(): # stdout flush fails on Mac |
| 93 | logging.info("flushing stdout") |
| 94 | sys.stdout.flush() |
| 95 | |
| 96 | logging.info("collecting result code") |
| 97 | result = p.poll() |
| 98 | if result: |
| 99 | logging.error("%s exited with non-zero result code %d" % (proc[0], result)) |
| 100 | return result |
| 101 | |
| 102 | |
| 103 | def IsLinux(): |
| 104 | return sys.platform.startswith('linux') |
| 105 | |
| 106 | |
| 107 | def IsMac(): |
| 108 | return sys.platform.startswith('darwin') |
| 109 | |
| 110 | |
| 111 | def IsWindows(): |
| 112 | return sys.platform == 'cygwin' or sys.platform.startswith('win') |
| 113 | |
| 114 | |
| 115 | def WindowsVersionName(): |
| 116 | """Returns the name of the Windows version if it is known, or None. |
| 117 | |
| 118 | Possible return values are: xp, vista, 7, 8, or None |
| 119 | """ |
| 120 | if sys.platform == 'cygwin': |
| 121 | # Windows version number is hiding in system name. Looks like: |
| 122 | # CYGWIN_NT-6.1-WOW64 |
| 123 | try: |
| 124 | version_str = platform.uname()[0].split('-')[1] |
| 125 | except: |
| 126 | return None |
| 127 | elif sys.platform.startswith('win'): |
| 128 | # Normal Windows version string. Mine: 6.1.7601 |
| 129 | version_str = platform.version() |
| 130 | else: |
| 131 | return None |
| 132 | |
| 133 | parts = version_str.split('.') |
| 134 | try: |
| 135 | major = int(parts[0]) |
| 136 | minor = int(parts[1]) |
| 137 | except: |
| 138 | return None # Can't parse, unknown version. |
| 139 | |
| 140 | if major == 5: |
| 141 | return 'xp' |
| 142 | elif major == 6 and minor == 0: |
| 143 | return 'vista' |
| 144 | elif major == 6 and minor == 1: |
| 145 | return '7' |
| 146 | elif major == 6 and minor == 2: |
| 147 | return '8' # Future proof. ;) |
| 148 | return None |
| 149 | |
| 150 | |
| 151 | def PlatformNames(): |
| 152 | """Return an array of string to be used in paths for the platform |
| 153 | (e.g. suppressions, gtest filters, ignore files etc.) |
| 154 | The first element of the array describes the 'main' platform |
| 155 | """ |
| 156 | if IsLinux(): |
| 157 | return ['linux'] |
| 158 | if IsMac(): |
| 159 | return ['mac'] |
| 160 | if IsWindows(): |
| 161 | names = ['win32'] |
| 162 | version_name = WindowsVersionName() |
| 163 | if version_name is not None: |
| 164 | names.append('win-%s' % version_name) |
| 165 | return names |
| 166 | raise NotImplementedError('Unknown platform "%s".' % sys.platform) |
| 167 | |
| 168 | |
| 169 | def PutEnvAndLog(env_name, env_value): |
| 170 | os.putenv(env_name, env_value) |
| 171 | logging.info('export %s=%s', env_name, env_value) |
| 172 | |
| 173 | def BoringCallers(mangled, use_re_wildcards): |
| 174 | """Return a list of 'boring' function names (optinally mangled) |
| 175 | with */? wildcards (optionally .*/.). |
| 176 | Boring = we drop off the bottom of stack traces below such functions. |
| 177 | """ |
| 178 | |
| 179 | need_mangling = [ |
| 180 | # Don't show our testing framework: |
| 181 | ("testing::Test::Run", "_ZN7testing4Test3RunEv"), |
| 182 | ("testing::TestInfo::Run", "_ZN7testing8TestInfo3RunEv"), |
| 183 | ("testing::internal::Handle*ExceptionsInMethodIfSupported*", |
| 184 | "_ZN7testing8internal3?Handle*ExceptionsInMethodIfSupported*"), |
| 185 | |
| 186 | # Depend on scheduling: |
| 187 | ("MessageLoop::Run", "_ZN11MessageLoop3RunEv"), |
| 188 | ("MessageLoop::RunTask", "_ZN11MessageLoop7RunTask*"), |
| 189 | ("RunnableMethod*", "_ZN14RunnableMethod*"), |
| 190 | ("DispatchToMethod*", "_Z*16DispatchToMethod*"), |
| 191 | ("base::internal::Invoker*::DoInvoke*", |
| 192 | "_ZN4base8internal8Invoker*DoInvoke*"), # Invoker{1,2,3} |
| 193 | ("base::internal::RunnableAdapter*::Run*", |
| 194 | "_ZN4base8internal15RunnableAdapter*Run*"), |
| 195 | ] |
| 196 | |
| 197 | ret = [] |
| 198 | for pair in need_mangling: |
| 199 | ret.append(pair[1 if mangled else 0]) |
| 200 | |
| 201 | ret += [ |
| 202 | # Also don't show the internals of libc/pthread. |
| 203 | "start_thread", |
| 204 | "main", |
| 205 | "BaseThreadInitThunk", |
| 206 | ] |
| 207 | |
| 208 | if use_re_wildcards: |
| 209 | for i in range(0, len(ret)): |
| 210 | ret[i] = ret[i].replace('*', '.*').replace('?', '.') |
| 211 | |
| 212 | return ret |
| 213 | |
| 214 | def NormalizeWindowsPath(path): |
| 215 | """If we're using Cygwin Python, turn the path into a Windows path. |
| 216 | |
| 217 | Don't turn forward slashes into backslashes for easier copy-pasting and |
| 218 | escaping. |
| 219 | |
| 220 | TODO(rnk): If we ever want to cut out the subprocess invocation, we can use |
| 221 | _winreg to get the root Cygwin directory from the registry key: |
| 222 | HKEY_LOCAL_MACHINE\SOFTWARE\Cygwin\setup\rootdir. |
| 223 | """ |
| 224 | if sys.platform.startswith("cygwin"): |
| 225 | p = subprocess.Popen(["cygpath", "-m", path], |
| 226 | stdout=subprocess.PIPE, |
| 227 | stderr=subprocess.PIPE) |
| 228 | (out, err) = p.communicate() |
| 229 | if err: |
| 230 | logging.warning("WARNING: cygpath error: %s", err) |
| 231 | return out.strip() |
| 232 | else: |
| 233 | return path |
| 234 | |
| 235 | ############################ |
| 236 | # Common output format code |
| 237 | |
| 238 | def PrintUsedSuppressionsList(suppcounts): |
| 239 | """ Prints out the list of used suppressions in a format common to all the |
| 240 | memory tools. If the list is empty, prints nothing and returns False, |
| 241 | otherwise True. |
| 242 | |
| 243 | suppcounts: a dictionary of used suppression counts, |
| 244 | Key -> name, Value -> count. |
| 245 | """ |
| 246 | if not suppcounts: |
| 247 | return False |
| 248 | |
| 249 | print "-----------------------------------------------------" |
| 250 | print "Suppressions used:" |
| 251 | print " count name" |
| 252 | for (name, count) in sorted(suppcounts.items(), key=lambda (k,v): (v,k)): |
| 253 | print "%7d %s" % (count, name) |
| 254 | print "-----------------------------------------------------" |
| 255 | sys.stdout.flush() |
| 256 | return True |