| # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import logging | |
| import platform | |
| import os | |
| import signal | |
| import subprocess | |
| import sys | |
| import time | |
| class NotImplementedError(Exception): | |
| pass | |
| class TimeoutError(Exception): | |
| pass | |
| def RunSubprocessInBackground(proc): | |
| """Runs a subprocess in the background. Returns a handle to the process.""" | |
| logging.info("running %s in the background" % " ".join(proc)) | |
| return subprocess.Popen(proc) | |
| def RunSubprocess(proc, timeout=0): | |
| """ Runs a subprocess, until it finishes or |timeout| is exceeded and the | |
| process is killed with taskkill. A |timeout| <= 0 means no timeout. | |
| Args: | |
| proc: list of process components (exe + args) | |
| timeout: how long to wait before killing, <= 0 means wait forever | |
| """ | |
| logging.info("running %s, timeout %d sec" % (" ".join(proc), timeout)) | |
| sys.stdout.flush() | |
| sys.stderr.flush() | |
| # Manually read and print out stdout and stderr. | |
| # By default, the subprocess is supposed to inherit these from its parent, | |
| # however when run under buildbot, it seems unable to read data from a | |
| # grandchild process, so we have to read the child and print the data as if | |
| # it came from us for buildbot to read it. We're not sure why this is | |
| # necessary. | |
| # TODO(erikkay): should we buffer stderr and stdout separately? | |
| p = subprocess.Popen(proc, universal_newlines=True, | |
| bufsize=0, # unbuffered | |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
| logging.info("started subprocess") | |
| did_timeout = False | |
| if timeout > 0: | |
| wait_until = time.time() + timeout | |
| while p.poll() is None and not did_timeout: | |
| # Have to use readline rather than readlines() or "for line in p.stdout:", | |
| # otherwise we get buffered even with bufsize=0. | |
| line = p.stdout.readline() | |
| while line and not did_timeout: | |
| sys.stdout.write(line) | |
| sys.stdout.flush() | |
| line = p.stdout.readline() | |
| if timeout > 0: | |
| did_timeout = time.time() > wait_until | |
| if did_timeout: | |
| logging.info("process timed out") | |
| else: | |
| logging.info("process ended, did not time out") | |
| if did_timeout: | |
| if IsWindows(): | |
| subprocess.call(["taskkill", "/T", "/F", "/PID", str(p.pid)]) | |
| else: | |
| # Does this kill all children, too? | |
| os.kill(p.pid, signal.SIGINT) | |
| logging.error("KILLED %d" % p.pid) | |
| # Give the process a chance to actually die before continuing | |
| # so that cleanup can happen safely. | |
| time.sleep(1.0) | |
| logging.error("TIMEOUT waiting for %s" % proc[0]) | |
| raise TimeoutError(proc[0]) | |
| else: | |
| for line in p.stdout: | |
| sys.stdout.write(line) | |
| if not IsMac(): # stdout flush fails on Mac | |
| logging.info("flushing stdout") | |
| sys.stdout.flush() | |
| logging.info("collecting result code") | |
| result = p.poll() | |
| if result: | |
| logging.error("%s exited with non-zero result code %d" % (proc[0], result)) | |
| return result | |
| def IsLinux(): | |
| return sys.platform.startswith('linux') | |
| def IsMac(): | |
| return sys.platform.startswith('darwin') | |
| def IsWindows(): | |
| return sys.platform == 'cygwin' or sys.platform.startswith('win') | |
| def WindowsVersionName(): | |
| """Returns the name of the Windows version if it is known, or None. | |
| Possible return values are: xp, vista, 7, 8, or None | |
| """ | |
| if sys.platform == 'cygwin': | |
| # Windows version number is hiding in system name. Looks like: | |
| # CYGWIN_NT-6.1-WOW64 | |
| try: | |
| version_str = platform.uname()[0].split('-')[1] | |
| except: | |
| return None | |
| elif sys.platform.startswith('win'): | |
| # Normal Windows version string. Mine: 6.1.7601 | |
| version_str = platform.version() | |
| else: | |
| return None | |
| parts = version_str.split('.') | |
| try: | |
| major = int(parts[0]) | |
| minor = int(parts[1]) | |
| except: | |
| return None # Can't parse, unknown version. | |
| if major == 5: | |
| return 'xp' | |
| elif major == 6 and minor == 0: | |
| return 'vista' | |
| elif major == 6 and minor == 1: | |
| return '7' | |
| elif major == 6 and minor == 2: | |
| return '8' # Future proof. ;) | |
| return None | |
| def PlatformNames(): | |
| """Return an array of string to be used in paths for the platform | |
| (e.g. suppressions, gtest filters, ignore files etc.) | |
| The first element of the array describes the 'main' platform | |
| """ | |
| if IsLinux(): | |
| return ['linux'] | |
| if IsMac(): | |
| return ['mac'] | |
| if IsWindows(): | |
| names = ['win32'] | |
| version_name = WindowsVersionName() | |
| if version_name is not None: | |
| names.append('win-%s' % version_name) | |
| return names | |
| raise NotImplementedError('Unknown platform "%s".' % sys.platform) | |
| def PutEnvAndLog(env_name, env_value): | |
| os.putenv(env_name, env_value) | |
| logging.info('export %s=%s', env_name, env_value) | |
| def BoringCallers(mangled, use_re_wildcards): | |
| """Return a list of 'boring' function names (optinally mangled) | |
| with */? wildcards (optionally .*/.). | |
| Boring = we drop off the bottom of stack traces below such functions. | |
| """ | |
| need_mangling = [ | |
| # Don't show our testing framework: | |
| ("testing::Test::Run", "_ZN7testing4Test3RunEv"), | |
| ("testing::TestInfo::Run", "_ZN7testing8TestInfo3RunEv"), | |
| ("testing::internal::Handle*ExceptionsInMethodIfSupported*", | |
| "_ZN7testing8internal3?Handle*ExceptionsInMethodIfSupported*"), | |
| # Depend on scheduling: | |
| ("MessageLoop::Run", "_ZN11MessageLoop3RunEv"), | |
| ("MessageLoop::RunTask", "_ZN11MessageLoop7RunTask*"), | |
| ("RunnableMethod*", "_ZN14RunnableMethod*"), | |
| ("DispatchToMethod*", "_Z*16DispatchToMethod*"), | |
| ("base::internal::Invoker*::DoInvoke*", | |
| "_ZN4base8internal8Invoker*DoInvoke*"), # Invoker{1,2,3} | |
| ("base::internal::RunnableAdapter*::Run*", | |
| "_ZN4base8internal15RunnableAdapter*Run*"), | |
| ] | |
| ret = [] | |
| for pair in need_mangling: | |
| ret.append(pair[1 if mangled else 0]) | |
| ret += [ | |
| # Also don't show the internals of libc/pthread. | |
| "start_thread", | |
| "main", | |
| "BaseThreadInitThunk", | |
| ] | |
| if use_re_wildcards: | |
| for i in range(0, len(ret)): | |
| ret[i] = ret[i].replace('*', '.*').replace('?', '.') | |
| return ret | |
| def NormalizeWindowsPath(path): | |
| """If we're using Cygwin Python, turn the path into a Windows path. | |
| Don't turn forward slashes into backslashes for easier copy-pasting and | |
| escaping. | |
| TODO(rnk): If we ever want to cut out the subprocess invocation, we can use | |
| _winreg to get the root Cygwin directory from the registry key: | |
| HKEY_LOCAL_MACHINE\SOFTWARE\Cygwin\setup\rootdir. | |
| """ | |
| if sys.platform.startswith("cygwin"): | |
| p = subprocess.Popen(["cygpath", "-m", path], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| (out, err) = p.communicate() | |
| if err: | |
| logging.warning("WARNING: cygpath error: %s", err) | |
| return out.strip() | |
| else: | |
| return path | |
| ############################ | |
| # Common output format code | |
| def PrintUsedSuppressionsList(suppcounts): | |
| """ Prints out the list of used suppressions in a format common to all the | |
| memory tools. If the list is empty, prints nothing and returns False, | |
| otherwise True. | |
| suppcounts: a dictionary of used suppression counts, | |
| Key -> name, Value -> count. | |
| """ | |
| if not suppcounts: | |
| return False | |
| print "-----------------------------------------------------" | |
| print "Suppressions used:" | |
| print " count name" | |
| for (name, count) in sorted(suppcounts.items(), key=lambda (k,v): (v,k)): | |
| print "%7d %s" % (count, name) | |
| print "-----------------------------------------------------" | |
| sys.stdout.flush() | |
| return True |