summaryrefslogtreecommitdiff
path: root/tools/drmemory/scripts/common.py
blob: 7e163e3c6025354c11092f6939509c999b4f733b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# 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