1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Runs an exe through Valgrind and puts the intermediate files in a 6 directory. 7 """ 8 9 import datetime 10 import glob 11 import logging 12 import optparse 13 import os 14 import re 15 import shutil 16 import stat 17 import subprocess 18 import sys 19 import tempfile 20 21 import common 22 23 import drmemory_analyze 24 25 class BaseTool(object): 26 """Abstract class for running dynamic error detection tools. 27 28 Always subclass this and implement ToolCommand with framework- and 29 tool-specific stuff. 30 """ 31 32 def __init__(self): 33 temp_parent_dir = None 34 self.log_parent_dir = "" 35 if common.IsWindows(): 36 # gpu process on Windows Vista+ runs at Low Integrity and can only 37 # write to certain directories (http://crbug.com/119131) 38 # 39 # TODO(bruening): if scripts die in middle and don't clean up temp 40 # dir, we'll accumulate files in profile dir. should remove 41 # really old files automatically. 42 profile = os.getenv("USERPROFILE") 43 if profile: 44 self.log_parent_dir = profile + "\\AppData\\LocalLow\\" 45 if os.path.exists(self.log_parent_dir): 46 self.log_parent_dir = common.NormalizeWindowsPath(self.log_parent_dir) 47 temp_parent_dir = self.log_parent_dir 48 # Generated every time (even when overridden) 49 self.temp_dir = tempfile.mkdtemp(prefix="vg_logs_", dir=temp_parent_dir) 50 self.log_dir = self.temp_dir # overridable by --keep_logs 51 self.option_parser_hooks = [] 52 # TODO(glider): we may not need some of the env vars on some of the 53 # platforms. 54 self._env = { 55 "G_SLICE" : "always-malloc", 56 "NSS_DISABLE_UNLOAD" : "1", 57 "NSS_DISABLE_ARENA_FREE_LIST" : "1", 58 "GTEST_DEATH_TEST_USE_FORK": "1", 59 } 60 61 def ToolName(self): 62 raise NotImplementedError, "This method should be implemented " \ 63 "in the tool-specific subclass" 64 65 def Analyze(self, check_sanity=False): 66 raise NotImplementedError, "This method should be implemented " \ 67 "in the tool-specific subclass" 68 69 def RegisterOptionParserHook(self, hook): 70 # Frameworks and tools can add their own flags to the parser. 71 self.option_parser_hooks.append(hook) 72 73 def CreateOptionParser(self): 74 # Defines Chromium-specific flags. 75 self._parser = optparse.OptionParser("usage: %prog [options] <program to " 76 "test>") 77 self._parser.disable_interspersed_args() 78 self._parser.add_option("-t", "--timeout", 79 dest="timeout", metavar="TIMEOUT", default=100000, 80 help="timeout in seconds for the run (default 100000)") 81 self._parser.add_option("", "--build-dir", 82 help="the location of the compiler output") 83 self._parser.add_option("", "--source-dir", 84 help="path to top of source tree for this build" 85 "(used to normalize source paths in baseline)") 86 self._parser.add_option("", "--gtest_filter", default="", 87 help="which test case to run") 88 self._parser.add_option("", "--gtest_repeat", 89 help="how many times to run each test") 90 self._parser.add_option("", "--gtest_print_time", action="store_true", 91 default=False, 92 help="show how long each test takes") 93 self._parser.add_option("", "--ignore_exit_code", action="store_true", 94 default=False, 95 help="ignore exit code of the test " 96 "(e.g. test failures)") 97 self._parser.add_option("", "--keep_logs", action="store_true", 98 default=False, 99 help="store memory tool logs in the <tool>.logs " 100 "directory instead of /tmp.\nThis can be " 101 "useful for tool developers/maintainers.\n" 102 "Please note that the <tool>.logs directory " 103 "will be clobbered on tool startup.") 104 105 # To add framework- or tool-specific flags, please add a hook using 106 # RegisterOptionParserHook in the corresponding subclass. 107 # See ValgrindTool for an example. 108 for hook in self.option_parser_hooks: 109 hook(self, self._parser) 110 111 def ParseArgv(self, args): 112 self.CreateOptionParser() 113 114 # self._tool_flags will store those tool flags which we don't parse 115 # manually in this script. 116 self._tool_flags = [] 117 known_args = [] 118 119 """ We assume that the first argument not starting with "-" is a program 120 name and all the following flags should be passed to the program. 121 TODO(timurrrr): customize optparse instead 122 """ 123 while len(args) > 0 and args[0][:1] == "-": 124 arg = args[0] 125 if (arg == "--"): 126 break 127 if self._parser.has_option(arg.split("=")[0]): 128 known_args += [arg] 129 else: 130 self._tool_flags += [arg] 131 args = args[1:] 132 133 if len(args) > 0: 134 known_args += args 135 136 self._options, self._args = self._parser.parse_args(known_args) 137 138 self._timeout = int(self._options.timeout) 139 self._source_dir = self._options.source_dir 140 if self._options.keep_logs: 141 # log_parent_dir has trailing slash if non-empty 142 self.log_dir = self.log_parent_dir + "%s.logs" % self.ToolName() 143 if os.path.exists(self.log_dir): 144 shutil.rmtree(self.log_dir) 145 os.mkdir(self.log_dir) 146 logging.info("Logs are in " + self.log_dir) 147 148 self._ignore_exit_code = self._options.ignore_exit_code 149 if self._options.gtest_filter != "": 150 self._args.append("--gtest_filter=%s" % self._options.gtest_filter) 151 if self._options.gtest_repeat: 152 self._args.append("--gtest_repeat=%s" % self._options.gtest_repeat) 153 if self._options.gtest_print_time: 154 self._args.append("--gtest_print_time") 155 156 return True 157 158 def Setup(self, args): 159 return self.ParseArgv(args) 160 161 def ToolCommand(self): 162 raise NotImplementedError, "This method should be implemented " \ 163 "in the tool-specific subclass" 164 165 def Cleanup(self): 166 # You may override it in the tool-specific subclass 167 pass 168 169 def Execute(self): 170 """ Execute the app to be tested after successful instrumentation. 171 Full execution command-line provided by subclassers via proc.""" 172 logging.info("starting execution...") 173 proc = self.ToolCommand() 174 for var in self._env: 175 common.PutEnvAndLog(var, self._env[var]) 176 return common.RunSubprocess(proc, self._timeout) 177 178 def RunTestsAndAnalyze(self, check_sanity): 179 exec_retcode = self.Execute() 180 analyze_retcode = self.Analyze(check_sanity) 181 182 if analyze_retcode: 183 logging.error("Analyze failed.") 184 logging.info("Search the log for '[ERROR]' to see the error reports.") 185 return analyze_retcode 186 187 if exec_retcode: 188 if self._ignore_exit_code: 189 logging.info("Test execution failed, but the exit code is ignored.") 190 else: 191 logging.error("Test execution failed.") 192 return exec_retcode 193 else: 194 logging.info("Test execution completed successfully.") 195 196 if not analyze_retcode: 197 logging.info("Analysis completed successfully.") 198 199 return 0 200 201 def Main(self, args, check_sanity, min_runtime_in_seconds): 202 """Call this to run through the whole process: Setup, Execute, Analyze""" 203 start_time = datetime.datetime.now() 204 retcode = -1 205 if self.Setup(args): 206 retcode = self.RunTestsAndAnalyze(check_sanity) 207 shutil.rmtree(self.temp_dir, ignore_errors=True) 208 self.Cleanup() 209 else: 210 logging.error("Setup failed") 211 end_time = datetime.datetime.now() 212 runtime_in_seconds = (end_time - start_time).seconds 213 hours = runtime_in_seconds / 3600 214 seconds = runtime_in_seconds % 3600 215 minutes = seconds / 60 216 seconds = seconds % 60 217 logging.info("elapsed time: %02d:%02d:%02d" % (hours, minutes, seconds)) 218 if (min_runtime_in_seconds > 0 and 219 runtime_in_seconds < min_runtime_in_seconds): 220 logging.error("Layout tests finished too quickly. " 221 "It should have taken at least %d seconds. " 222 "Something went wrong?" % min_runtime_in_seconds) 223 retcode = -1 224 return retcode 225 226 def Run(self, args, module, min_runtime_in_seconds=0): 227 MODULES_TO_SANITY_CHECK = ["base"] 228 229 check_sanity = module in MODULES_TO_SANITY_CHECK 230 return self.Main(args, check_sanity, min_runtime_in_seconds) 231 232 233 class DrMemory(BaseTool): 234 """Dr.Memory 235 Dynamic memory error detector for Windows. 236 237 http://dev.chromium.org/developers/how-tos/using-drmemory 238 It is not very mature at the moment, some things might not work properly. 239 """ 240 241 def __init__(self, full_mode, pattern_mode): 242 super(DrMemory, self).__init__() 243 self.full_mode = full_mode 244 self.pattern_mode = pattern_mode 245 self.RegisterOptionParserHook(DrMemory.ExtendOptionParser) 246 247 def ToolName(self): 248 return "drmemory" 249 250 def ExtendOptionParser(self, parser): 251 parser.add_option("", "--suppressions", default=[], 252 action="append", 253 help="path to a drmemory suppression file") 254 parser.add_option("", "--follow_python", action="store_true", 255 default=False, dest="follow_python", 256 help="Monitor python child processes. If off, neither " 257 "python children nor any children of python children " 258 "will be monitored.") 259 parser.add_option("", "--indirect_pdfium_test", action="store_true", 260 default=False, 261 help="set --wrapper rather than running Dr. Memory " 262 "directly.") 263 parser.add_option("", "--use_debug", action="store_true", 264 default=False, dest="use_debug", 265 help="Run Dr. Memory debug build") 266 parser.add_option("", "--trace_children", action="store_true", 267 default=True, 268 help="TODO: default value differs from Valgrind") 269 270 def ToolCommand(self): 271 """Get the tool command to run.""" 272 # WINHEAP is what Dr. Memory supports as there are issues w/ both 273 # jemalloc (https://github.com/DynamoRIO/drmemory/issues/320) and 274 # tcmalloc (https://github.com/DynamoRIO/drmemory/issues/314) 275 add_env = { 276 "CHROME_ALLOCATOR" : "WINHEAP", 277 "JSIMD_FORCEMMX" : "1", # https://github.com/DynamoRIO/drmemory/issues/540 278 } 279 for k,v in add_env.iteritems(): 280 logging.info("export %s=%s", k, v) 281 os.putenv(k, v) 282 283 drmem_cmd = os.getenv("DRMEMORY_COMMAND") 284 if not drmem_cmd: 285 raise RuntimeError, "Please set DRMEMORY_COMMAND environment variable " \ 286 "with the path to drmemory.exe" 287 proc = drmem_cmd.split(" ") 288 289 # By default, don't run python (this will exclude python's children as well) 290 # to reduce runtime. We're not really interested in spending time finding 291 # bugs in the python implementation. 292 # With file-based config we must update the file every time, and 293 # it will affect simultaneous drmem uses by this user. While file-based 294 # config has many advantages, here we may want this-instance-only 295 # (https://github.com/DynamoRIO/drmemory/issues/334). 296 drconfig_cmd = [ proc[0].replace("drmemory.exe", "drconfig.exe") ] 297 drconfig_cmd += ["-quiet"] # suppress errors about no 64-bit libs 298 run_drconfig = True 299 if self._options.follow_python: 300 logging.info("Following python children") 301 # -unreg fails if not already registered so query for that first 302 query_cmd = drconfig_cmd + ["-isreg", "python.exe"] 303 query_proc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE, 304 shell=True) 305 (query_out, query_err) = query_proc.communicate() 306 if re.search("exe not registered", query_out): 307 run_drconfig = False # all set 308 else: 309 drconfig_cmd += ["-unreg", "python.exe"] 310 else: 311 logging.info("Excluding python children") 312 drconfig_cmd += ["-reg", "python.exe", "-norun"] 313 if run_drconfig: 314 drconfig_retcode = common.RunSubprocess(drconfig_cmd, self._timeout) 315 if drconfig_retcode: 316 logging.error("Configuring whether to follow python children failed " \ 317 "with %d.", drconfig_retcode) 318 raise RuntimeError, "Configuring python children failed " 319 320 suppression_count = 0 321 supp_files = self._options.suppressions 322 if self.full_mode: 323 supp_files += [s.replace(".txt", "_full.txt") for s in supp_files] 324 for suppression_file in supp_files: 325 if os.path.exists(suppression_file): 326 suppression_count += 1 327 proc += ["-suppress", common.NormalizeWindowsPath(suppression_file)] 328 329 if not suppression_count: 330 logging.warning("WARNING: NOT USING SUPPRESSIONS!") 331 332 # Un-comment to dump Dr.Memory events on error 333 #proc += ["-dr_ops", "-dumpcore_mask", "-dr_ops", "0x8bff"] 334 335 # Un-comment and comment next line to debug Dr.Memory 336 #proc += ["-dr_ops", "-no_hide"] 337 #proc += ["-dr_ops", "-msgbox_mask", "-dr_ops", "15"] 338 #Proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "15"] 339 # Ensure we see messages about Dr. Memory crashing! 340 proc += ["-dr_ops", "-stderr_mask", "-dr_ops", "12"] 341 342 if self._options.use_debug: 343 proc += ["-debug"] 344 345 proc += ["-logdir", common.NormalizeWindowsPath(self.log_dir)] 346 347 if self.log_parent_dir: 348 # gpu process on Windows Vista+ runs at Low Integrity and can only 349 # write to certain directories (http://crbug.com/119131) 350 symcache_dir = os.path.join(self.log_parent_dir, "drmemory.symcache") 351 elif self._options.build_dir: 352 # The other case is only possible with -t cmdline. 353 # Anyways, if we omit -symcache_dir the -logdir's value is used which 354 # should be fine. 355 symcache_dir = os.path.join(self._options.build_dir, "drmemory.symcache") 356 if symcache_dir: 357 if not os.path.exists(symcache_dir): 358 try: 359 os.mkdir(symcache_dir) 360 except OSError: 361 logging.warning("Can't create symcache dir?") 362 if os.path.exists(symcache_dir): 363 proc += ["-symcache_dir", common.NormalizeWindowsPath(symcache_dir)] 364 365 # Use -no_summary to suppress DrMemory's summary and init-time 366 # notifications. We generate our own with drmemory_analyze.py. 367 proc += ["-batch", "-no_summary"] 368 369 # Un-comment to disable interleaved output. Will also suppress error 370 # messages normally printed to stderr. 371 #proc += ["-quiet", "-no_results_to_stderr"] 372 373 proc += ["-callstack_max_frames", "40"] 374 375 # disable leak scan for now 376 proc += ["-no_count_leaks", "-no_leak_scan"] 377 378 # disable warnings about unaddressable prefetches 379 proc += ["-no_check_prefetch"] 380 381 # crbug.com/413215, no heap mismatch check for Windows release build binary 382 if common.IsWindows() and "Release" in self._options.build_dir: 383 proc += ["-no_check_delete_mismatch"] 384 385 # make callstacks easier to read 386 proc += ["-callstack_srcfile_prefix", 387 "build\\src,chromium\\src,crt_build\\self_x86"] 388 proc += ["-callstack_modname_hide", 389 "*drmemory*,chrome.dll"] 390 391 boring_callers = common.BoringCallers(mangled=False, use_re_wildcards=False) 392 # TODO(timurrrr): In fact, we want "starting from .." instead of "below .." 393 proc += ["-callstack_truncate_below", ",".join(boring_callers)] 394 395 if self.pattern_mode: 396 proc += ["-pattern", "0xf1fd", "-no_count_leaks", "-redzone_size", "0x20"] 397 elif not self.full_mode: 398 proc += ["-light"] 399 400 proc += self._tool_flags 401 402 # Dr.Memory requires -- to separate tool flags from the executable name. 403 proc += ["--"] 404 405 if self._options.indirect_pdfium_test: 406 wrapper = " ".join(proc) 407 logging.info("pdfium wrapper = " + wrapper) 408 proc = self._args 409 proc += ["--wrapper", wrapper] 410 return proc 411 412 # Note that self._args begins with the name of the exe to be run. 413 self._args[0] = common.NormalizeWindowsPath(self._args[0]) 414 proc += self._args 415 return proc 416 417 def CreateBrowserWrapper(self, command): 418 os.putenv("BROWSER_WRAPPER", command) 419 420 def Analyze(self, check_sanity=False): 421 # Use one analyzer for all the log files to avoid printing duplicate reports 422 # 423 # TODO(timurrrr): unify this with Valgrind and other tools when we have 424 # https://github.com/DynamoRIO/drmemory/issues/684 425 analyzer = drmemory_analyze.DrMemoryAnalyzer() 426 427 ret = 0 428 if not self._options.indirect_pdfium_test: 429 filenames = glob.glob(self.log_dir + "/*/results.txt") 430 431 ret = analyzer.Report(filenames, None, check_sanity) 432 else: 433 testcases = glob.glob(self.log_dir + "/testcase.*.logs") 434 # If we have browser wrapper, the per-test logdirs are named as 435 # "testcase.wrapper_PID.name". 436 # Let's extract the list of wrapper_PIDs and name it ppids. 437 # NOTE: ppids may contain '_', i.e. they are not ints! 438 ppids = set([f.split(".")[-2] for f in testcases]) 439 440 for ppid in ppids: 441 testcase_name = None 442 try: 443 f = open("%s/testcase.%s.name" % (self.log_dir, ppid)) 444 testcase_name = f.read().strip() 445 f.close() 446 except IOError: 447 pass 448 print "=====================================================" 449 print " Below is the report for drmemory wrapper PID=%s." % ppid 450 if testcase_name: 451 print " It was used while running the `%s` test." % testcase_name 452 else: 453 # TODO(timurrrr): hm, the PID line is suppressed on Windows... 454 print " You can find the corresponding test" 455 print " by searching the above log for 'PID=%s'" % ppid 456 sys.stdout.flush() 457 ppid_filenames = glob.glob("%s/testcase.%s.logs/*/results.txt" % 458 (self.log_dir, ppid)) 459 ret |= analyzer.Report(ppid_filenames, testcase_name, False) 460 print "=====================================================" 461 sys.stdout.flush() 462 463 logging.info("Please see http://dev.chromium.org/developers/how-tos/" 464 "using-drmemory for the info on Dr. Memory") 465 return ret 466 467 468 class ToolFactory: 469 def Create(self, tool_name): 470 if tool_name == "drmemory" or tool_name == "drmemory_light": 471 # TODO(timurrrr): remove support for "drmemory" when buildbots are 472 # switched to drmemory_light OR make drmemory==drmemory_full the default 473 # mode when the tool is mature enough. 474 return DrMemory(False, False) 475 if tool_name == "drmemory_full": 476 return DrMemory(True, False) 477 if tool_name == "drmemory_pattern": 478 return DrMemory(False, True) 479 try: 480 platform_name = common.PlatformNames()[0] 481 except common.NotImplementedError: 482 platform_name = sys.platform + "(Unknown)" 483 raise RuntimeError, "Unknown tool (tool=%s, platform=%s)" % (tool_name, 484 platform_name) 485 486 def CreateTool(tool): 487 return ToolFactory().Create(tool) 488