| # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| # for details. All rights reserved. Use of this source code is governed by a |
| # BSD-style license that can be found in the LICENSE file. |
| |
| # This file contains a set of utilities functions used by other Python-based |
| # scripts. |
| |
| from __future__ import print_function |
| |
| import contextlib |
| import datetime |
| from functools import total_ordering |
| import glob |
| import importlib.util |
| import importlib.machinery |
| import json |
| import os |
| import platform |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tarfile |
| import tempfile |
| import uuid |
| |
| try: |
| # Not available on Windows. |
| import resource |
| except: |
| pass |
| |
| SEMANTIC_VERSION_PATTERN = r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' |
| |
| |
| # To eliminate clashing with older archived builds on bleeding edge we add |
| # a base number bigger the largest svn revision (this also gives us an easy |
| # way of seeing if an archive comes from git based or svn based commits). |
| GIT_NUMBER_BASE = 100000 |
| |
| # Mapping table between build mode and build configuration. |
| BUILD_MODES = { |
| 'debug': 'Debug', |
| 'release': 'Release', |
| 'product': 'Product', |
| } |
| |
| # Mapping table between build mode and build configuration. |
| BUILD_SANITIZERS = { |
| None: '', |
| 'none': '', |
| 'asan': 'ASAN', |
| 'lsan': 'LSAN', |
| 'msan': 'MSAN', |
| 'tsan': 'TSAN', |
| 'ubsan': 'UBSAN', |
| } |
| |
| # Mapping table between OS and build output location. |
| BUILD_ROOT = { |
| 'win32': 'out', |
| 'linux': 'out', |
| 'freebsd': 'out', |
| 'macos': 'xcodebuild', |
| } |
| |
| # Note: gn expects these to be lower case. |
| ARCH_FAMILY = { |
| 'ia32': 'ia32', |
| 'x64': 'ia32', |
| 'arm': 'arm', |
| 'arm64': 'arm', |
| 'arm_x64': 'arm', |
| 'arm_arm64': 'arm', |
| 'simarm': 'ia32', |
| 'simarm64': 'ia32', |
| 'simarm_x64': 'ia32', |
| 'simarm_arm64': 'arm', |
| 'x64c': 'ia32', |
| 'arm64c': 'arm', |
| 'simarm64c': 'ia32', |
| 'simriscv32': 'ia32', |
| 'simriscv64': 'ia32', |
| 'simx64': 'arm', |
| 'simx64c': 'arm', |
| 'riscv32': 'riscv', |
| 'riscv64': 'riscv', |
| } |
| |
| BASE_DIR = os.path.abspath(os.path.join(os.curdir, '..')) |
| DART_DIR = os.path.abspath(os.path.join(__file__, '..', '..')) |
| VERSION_FILE = os.path.join(DART_DIR, 'tools', 'VERSION') |
| |
| |
| def GetArchFamily(arch): |
| return ARCH_FAMILY[arch] |
| |
| |
| def GetBuildDir(host_os): |
| return BUILD_ROOT[host_os] |
| |
| |
| def GetBuildMode(mode): |
| return BUILD_MODES[mode] |
| |
| |
| def GetBuildSanitizer(sanitizer): |
| return BUILD_SANITIZERS[sanitizer] |
| |
| |
| def GetBaseDir(): |
| return BASE_DIR |
| |
| |
| def load_source(modname, filename): |
| loader = importlib.machinery.SourceFileLoader(modname, filename) |
| spec = importlib.util.spec_from_file_location(modname, |
| filename, |
| loader=loader) |
| module = importlib.util.module_from_spec(spec) |
| # The module is always executed and not cached in sys.modules. |
| # Uncomment the following line to cache the module. |
| # sys.modules[module.__name__] = module |
| loader.exec_module(module) |
| return module |
| |
| |
| def GetBotUtils(repo_path=DART_DIR): |
| '''Dynamically load the tools/bots/bot_utils.py python module.''' |
| return load_source('bot_utils', |
| os.path.join(repo_path, 'tools', 'bots', 'bot_utils.py')) |
| |
| |
| def GetMinidumpUtils(repo_path=DART_DIR): |
| '''Dynamically load the tools/minidump.py python module.''' |
| return load_source('minidump', |
| os.path.join(repo_path, 'tools', 'minidump.py')) |
| |
| |
| @total_ordering |
| class Version(object): |
| |
| def __init__(self, |
| channel=None, |
| major=None, |
| minor=None, |
| patch=None, |
| prerelease=None, |
| prerelease_patch=None, |
| version=None): |
| self.channel = channel |
| self.major = major |
| self.minor = minor |
| self.patch = patch |
| self.prerelease = prerelease |
| self.prerelease_patch = prerelease_patch |
| if version: |
| self.set_version(version) |
| |
| def set_version(self, version): |
| match = re.match(SEMANTIC_VERSION_PATTERN, version) |
| assert match, '%s must be a valid version' % version |
| self.channel = 'stable' |
| self.major = match['major'] |
| self.minor = match['minor'] |
| self.patch = match['patch'] |
| self.prerelease = '0' |
| self.prerelease_patch = '0' |
| if match['prerelease']: |
| subversions = match['prerelease'].split('.') |
| self.prerelease = subversions[0] |
| self.prerelease_patch = subversions[1] |
| self.channel = subversions[2] |
| |
| def __str__(self): |
| result = '%s.%s.%s' % (self.major, self.minor, self.patch) |
| if self.channel != 'stable': |
| result += '-%s.%s.%s' % (self.prerelease, self.prerelease_patch, |
| self.channel) |
| return result |
| |
| def __eq__(self, other): |
| return self.channel == other.channel and \ |
| self.major == other.major and \ |
| self.minor == other.minor and \ |
| self.patch == other.patch and \ |
| self.prerelease == other.prerelease and \ |
| self.prerelease_patch == other.prerelease_patch |
| |
| def __lt__(self, other): |
| if int(self.major) < int(other.major): |
| return True |
| if int(self.major) > int(other.major): |
| return False |
| if int(self.minor) < int(other.minor): |
| return True |
| if int(self.minor) > int(other.minor): |
| return False |
| if int(self.patch) < int(other.patch): |
| return True |
| if int(self.patch) > int(other.patch): |
| return False |
| # The stable channel is ahead of the other channels on the same triplet. |
| if self.channel != 'stable' and other.channel == 'stable': |
| return True |
| if self.channel == 'stable' and other.channel != 'stable': |
| return False |
| # The main channel is ahead of the other channels on the same triplet. |
| if self.channel != 'main' and other.channel == 'main': |
| return True |
| if self.channel == 'main' and other.channel != 'main': |
| return False |
| # The be channel existed before it was renamed to main. |
| if self.channel != 'be' and other.channel == 'be': |
| return True |
| if self.channel == 'be' and other.channel != 'be': |
| return False |
| if int(self.prerelease) < int(other.prerelease): |
| return True |
| if int(self.prerelease) > int(other.prerelease): |
| return False |
| if int(self.prerelease_patch) < int(other.prerelease_patch): |
| return True |
| if int(self.prerelease_patch) > int(other.prerelease_patch): |
| return False |
| return False |
| |
| |
| # Try to guess the host operating system. |
| def GuessOS(): |
| os_id = platform.system() |
| if os_id == 'Linux': |
| return 'linux' |
| elif os_id == 'Darwin': |
| return 'macos' |
| elif os_id == 'Windows' or os_id == 'Microsoft': |
| # On Windows Vista platform.system() can return 'Microsoft' with some |
| # versions of Python, see http://bugs.python.org/issue1082 for details. |
| return 'win32' |
| elif os_id == 'FreeBSD': |
| return 'freebsd' |
| elif os_id == 'OpenBSD': |
| return 'openbsd' |
| elif os_id == 'SunOS': |
| return 'solaris' |
| |
| return None |
| |
| |
| # Runs true if the currently executing python interpreter is running under |
| # Rosetta. I.e., python3 is an x64 executable and we're on an arm64 Mac. |
| def IsRosetta(): |
| if platform.system() == 'Darwin': |
| p = subprocess.Popen(['sysctl', '-in', 'sysctl.proc_translated'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| output, _ = p.communicate() |
| return output.decode('utf-8').strip() == '1' |
| return False |
| |
| |
| # Returns the architectures that can run on the current machine. |
| def HostArchitectures(): |
| m = platform.machine() |
| if platform.system() == 'Darwin': |
| if m == 'arm64' or IsRosetta(): |
| # ARM64 Macs also support X64. |
| return ['arm64', 'x64'] |
| if m == 'x86_64': |
| # X64 Macs no longer support IA32. |
| return ['x64'] |
| # Icky use of CIPD_ARCHITECTURE should be effectively dead whenever the |
| # Python on bots becomes native ARM64. |
| if ((platform.system() == 'Windows') and |
| (os.environ.get("CIPD_ARCHITECTURE") == "arm64")): |
| # ARM64 Windows also can emulate X64. |
| return ['arm64', 'x64'] |
| |
| if m in ['aarch64', 'arm64', 'arm64e', 'ARM64']: |
| return ['arm64'] |
| if m in ['armv7l', 'armv8l']: |
| return ['arm'] |
| if m in ['i386', 'i686', 'ia32', 'x86']: |
| return ['x86', 'ia32'] |
| if m in ['x64', 'x86-64', 'x86_64', 'amd64', 'AMD64']: |
| return ['x64', 'x86', 'ia32'] |
| if m in ['riscv64']: |
| return ['riscv64'] |
| raise Exception('Failed to determine host architectures for %s %s', |
| platform.machine(), platform.system()) |
| |
| |
| # Try to guess the host architecture. |
| def GuessArchitecture(): |
| return HostArchitectures()[0] |
| |
| # Try to guess the number of cpus on this machine. |
| def GuessCpus(): |
| if os.getenv('DART_NUMBER_OF_CORES') is not None: |
| return int(os.getenv('DART_NUMBER_OF_CORES')) |
| if os.path.exists('/proc/cpuinfo'): |
| return int( |
| subprocess.check_output( |
| 'grep -E \'^processor\' /proc/cpuinfo | wc -l', shell=True)) |
| if os.path.exists('/usr/bin/hostinfo'): |
| return int( |
| subprocess.check_output( |
| '/usr/bin/hostinfo |' |
| ' grep "processors are logically available." |' |
| ' awk "{ print \\$1 }"', |
| shell=True)) |
| win_cpu_count = os.getenv("NUMBER_OF_PROCESSORS") |
| if win_cpu_count: |
| return int(win_cpu_count) |
| return 2 |
| |
| |
| # Returns true if we're running under Windows. |
| def IsWindows(): |
| return GuessOS() == 'win32' |
| |
| |
| def IsCrossBuild(target_os, arch): |
| if (target_os not in [None, 'host']) and (target_os != GuessOS()): |
| return True |
| if arch.startswith('sim'): |
| return False |
| if arch.endswith('c'): |
| # Strip 'compressed' suffix. |
| arch = arch[:-1] |
| if arch in HostArchitectures(): |
| return False |
| return True |
| |
| |
| def GetBuildConf(mode, arch, conf_os=None, sanitizer=None): |
| if conf_os is not None and conf_os != GuessOS() and conf_os != 'host': |
| return '{}{}{}'.format(GetBuildMode(mode), conf_os.title(), |
| arch.upper()) |
| |
| # Ask for a cross build if the host and target architectures don't match. |
| cross_build = '' |
| if IsCrossBuild(conf_os, arch): |
| cross_build = 'X' |
| return '{}{}{}{}'.format(GetBuildMode(mode), GetBuildSanitizer(sanitizer), |
| cross_build, arch.upper()) |
| |
| |
| def GetBuildRoot(host_os, mode=None, arch=None, target_os=None, sanitizer=None): |
| build_root = GetBuildDir(host_os) |
| if mode: |
| build_root = os.path.join( |
| build_root, GetBuildConf(mode, arch, target_os, sanitizer)) |
| return build_root |
| |
| |
| def GetVersion(no_git_hash=False, version_file=None, git_revision_file=None): |
| version = ReadVersionFile(version_file) |
| if not version: |
| return None |
| |
| suffix = '' |
| if version.channel in ['main', 'be']: |
| suffix = '-edge' if no_git_hash else '-edge.{}'.format( |
| GetGitRevision(git_revision_file)) |
| elif version.channel in ('beta', 'dev'): |
| suffix = '-{}.{}.{}'.format(version.prerelease, |
| version.prerelease_patch, version.channel) |
| else: |
| assert version.channel == 'stable' |
| |
| return '{}.{}.{}{}'.format(version.major, version.minor, version.patch, |
| suffix) |
| |
| |
| def GetChannel(version_file=None): |
| version = ReadVersionFile(version_file) |
| return version.channel |
| |
| |
| def ReadVersionFile(version_file=None): |
| |
| def match_against(pattern, file_content): |
| match = re.search(pattern, file_content, flags=re.MULTILINE) |
| if match: |
| return match.group(1) |
| return None |
| |
| if version_file == None: |
| version_file = VERSION_FILE |
| |
| content = None |
| try: |
| with open(version_file) as fd: |
| content = fd.read() |
| except: |
| print('Warning: Could not read VERSION file ({})'.format(version_file)) |
| return None |
| |
| channel = match_against('^CHANNEL ([A-Za-z0-9]+)$', content) |
| major = match_against('^MAJOR (\\d+)$', content) |
| minor = match_against('^MINOR (\\d+)$', content) |
| patch = match_against('^PATCH (\\d+)$', content) |
| prerelease = match_against('^PRERELEASE (\\d+)$', content) |
| prerelease_patch = match_against('^PRERELEASE_PATCH (\\d+)$', content) |
| |
| if (channel and major and minor and prerelease and prerelease_patch): |
| return Version(channel, major, minor, patch, prerelease, |
| prerelease_patch) |
| |
| print('Warning: VERSION file ({}) has wrong format'.format(version_file)) |
| return None |
| |
| |
| def GetGitRevision(git_revision_file=None, repo_path=DART_DIR): |
| # When building from tarball use tools/GIT_REVISION |
| if git_revision_file is None: |
| git_revision_file = os.path.join(repo_path, 'tools', 'GIT_REVISION') |
| try: |
| with open(git_revision_file) as fd: |
| return fd.read().strip() |
| except: |
| pass |
| p = subprocess.Popen(['git', 'rev-parse', 'HEAD'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| shell=IsWindows(), |
| cwd=repo_path) |
| out, err = p.communicate() |
| # TODO(https://github.com/dart-lang/sdk/issues/51865): Don't ignore errors. |
| # if p.wait() != 0: |
| # raise Exception('git rev-parse failed: ' + str(err)) |
| revision = out.decode('utf-8').strip() |
| # We expect a full git hash |
| if len(revision) != 40: |
| print('Warning: Could not parse git commit, output was {}'.format( |
| revision), |
| file=sys.stderr) |
| return None |
| return revision |
| |
| |
| def GetShortGitHash(repo_path=DART_DIR): |
| p = subprocess.Popen(['git', 'rev-parse', '--short=10', 'HEAD'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| shell=IsWindows(), |
| cwd=repo_path) |
| out, err = p.communicate() |
| if p.wait() != 0: |
| # TODO(https://github.com/dart-lang/sdk/issues/51865): Don't ignore errors. |
| # raise Exception('git rev-parse failed: ' + str(err)) |
| return None |
| revision = out.decode('utf-8').strip() |
| return revision |
| |
| |
| def GetGitTimestamp(git_timestamp_file=None, repo_path=DART_DIR): |
| # When building from tarball use tools/GIT_TIMESTAMP |
| if git_timestamp_file is None: |
| git_timestamp_file = os.path.join(repo_path, 'tools', 'GIT_TIMESTAMP') |
| try: |
| with open(git_timestamp_file) as fd: |
| return fd.read().strip() |
| except: |
| pass |
| p = subprocess.Popen(['git', 'log', '-n', '1', '--pretty=format:%cd'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| shell=IsWindows(), |
| cwd=repo_path) |
| out, err = p.communicate() |
| if p.wait() != 0: |
| # TODO(https://github.com/dart-lang/sdk/issues/51865): Don't ignore errors. |
| # raise Exception('git log failed: ' + str(err)) |
| return None |
| timestamp = out.decode('utf-8').strip() |
| return timestamp |
| |
| |
| # TODO(42528): Can we remove this? It's basically just an alias for Exception. |
| class Error(Exception): |
| pass |
| |
| |
| def IsCrashExitCode(exit_code): |
| if IsWindows(): |
| return 0x80000000 & exit_code |
| return exit_code < 0 |
| |
| |
| def DiagnoseExitCode(exit_code, command): |
| if IsCrashExitCode(exit_code): |
| sys.stderr.write( |
| 'Command: {}\nCRASHED with exit code {} (0x{:x})\n'.format( |
| ' '.join(command), exit_code, exit_code & 0xffffffff)) |
| |
| |
| def CheckedInSdkPath(): |
| tools_dir = os.path.dirname(os.path.realpath(__file__)) |
| return os.path.join(tools_dir, 'sdks', 'dart-sdk') |
| |
| |
| def CheckedInSdkExecutable(): |
| name = 'dart' |
| if IsWindows(): |
| name = 'dart.exe' |
| return os.path.join(CheckedInSdkPath(), 'bin', name) |
| |
| |
| def CheckLinuxCoreDumpPattern(fatal=False): |
| core_pattern_file = '/proc/sys/kernel/core_pattern' |
| core_pattern = open(core_pattern_file).read() |
| |
| expected_core_pattern = 'core.%p' |
| if core_pattern.strip() != expected_core_pattern: |
| message = ( |
| 'Invalid core_pattern configuration. ' |
| 'The configuration of core dump handling is *not* correct for ' |
| 'a buildbot. The content of {0} must be "{1}" instead of "{2}".'. |
| format(core_pattern_file, expected_core_pattern, core_pattern)) |
| if fatal: |
| raise Exception(message) |
| print(message) |
| return False |
| return True |
| |
| |
| class TempDir(object): |
| |
| def __init__(self, prefix=''): |
| self._temp_dir = None |
| self._prefix = prefix |
| |
| def __enter__(self): |
| self._temp_dir = tempfile.mkdtemp(self._prefix) |
| return self._temp_dir |
| |
| def __exit__(self, *_): |
| shutil.rmtree(self._temp_dir, ignore_errors=True) |
| |
| |
| class UnexpectedCrash(object): |
| |
| def __init__(self, test, pid, *binaries): |
| self.test = test |
| self.pid = pid |
| self.binaries = binaries |
| |
| def __str__(self): |
| return 'Crash({}: {} {})'.format(self.test, self.pid, |
| ', '.join(self.binaries)) |
| |
| |
| class PosixCoreDumpEnabler(object): |
| |
| def __init__(self): |
| self._old_limits = None |
| |
| def __enter__(self): |
| self._old_limits = resource.getrlimit(resource.RLIMIT_CORE) |
| resource.setrlimit(resource.RLIMIT_CORE, (-1, -1)) |
| |
| def __exit__(self, *_): |
| if self._old_limits != None: |
| resource.setrlimit(resource.RLIMIT_CORE, self._old_limits) |
| |
| |
| class LinuxCoreDumpEnabler(PosixCoreDumpEnabler): |
| |
| def __enter__(self): |
| # Bump core limits to unlimited if core_pattern is correctly configured. |
| if CheckLinuxCoreDumpPattern(fatal=False): |
| super(LinuxCoreDumpEnabler, self).__enter__() |
| |
| def __exit__(self, *args): |
| CheckLinuxCoreDumpPattern(fatal=False) |
| super(LinuxCoreDumpEnabler, self).__exit__(*args) |
| |
| |
| class WindowsCoreDumpEnabler(object): |
| """This enabler assumes that Dart binary was built with Crashpad support. |
| In this case DART_CRASHPAD_CRASHES_DIR environment variable allows to |
| specify the location of Crashpad crashes database. Actual minidumps will |
| be written into reports subfolder of the database. |
| """ |
| CRASHPAD_DB_FOLDER = os.path.join(DART_DIR, 'crashes') |
| DUMPS_FOLDER = os.path.join(CRASHPAD_DB_FOLDER, 'reports') |
| |
| def __init__(self): |
| pass |
| |
| def __enter__(self): |
| print('INFO: Enabling coredump archiving into {}'.format( |
| WindowsCoreDumpEnabler.CRASHPAD_DB_FOLDER)) |
| os.environ[ |
| 'DART_CRASHPAD_CRASHES_DIR'] = WindowsCoreDumpEnabler.CRASHPAD_DB_FOLDER |
| |
| def __exit__(self, *_): |
| del os.environ['DART_CRASHPAD_CRASHES_DIR'] |
| |
| |
| def TryUnlink(file): |
| try: |
| os.unlink(file) |
| except Exception as error: |
| print('ERROR: Failed to remove {}: {}'.format(file, error)) |
| |
| |
| class BaseCoreDumpArchiver(object): |
| """This class reads coredumps file written by UnexpectedCrashDumpArchiver |
| into the current working directory and uploads all cores and binaries |
| listed in it into Cloud Storage (see |
| pkg/test_runner/lib/src/test_progress.dart). |
| """ |
| |
| # test.dart will write a line for each unexpected crash into this file. |
| _UNEXPECTED_CRASHES_FILE = 'unexpected-crashes' |
| |
| def __init__(self, search_dir, output_directory): |
| self._bucket = 'dart-temp-crash-archive' |
| self._binaries_dir = os.getcwd() |
| self._search_dir = search_dir |
| self._output_directory = output_directory |
| |
| def _safe_cleanup(self): |
| try: |
| return self._cleanup() |
| except Exception as error: |
| print('ERROR: Failure during cleanup: {}'.format(error)) |
| return False |
| |
| def __enter__(self): |
| print('INFO: Core dump archiving is activated') |
| |
| # Cleanup any stale files |
| if self._safe_cleanup(): |
| print('WARNING: Found and removed stale coredumps') |
| |
| def __exit__(self, *_): |
| try: |
| crashes = self._find_unexpected_crashes() |
| if crashes: |
| # If we get a ton of crashes, only archive 10 dumps. |
| archive_crashes = crashes[:10] |
| print('Archiving coredumps for crash (if possible):') |
| for crash in archive_crashes: |
| print('----> {}'.format(crash)) |
| |
| sys.stdout.flush() |
| |
| self._archive(archive_crashes) |
| else: |
| print('INFO: No unexpected crashes recorded') |
| dumps = self._find_all_coredumps() |
| if dumps: |
| print('INFO: However there are {} core dumps found'.format( |
| len(dumps))) |
| for dump in dumps: |
| print('INFO: -> {}'.format(dump)) |
| print() |
| except Exception as error: |
| print('ERROR: Failed to archive crashes: {}'.format(error)) |
| raise |
| |
| finally: |
| self._safe_cleanup() |
| |
| def _archive(self, crashes): |
| files = set() |
| missing = [] |
| for crash in crashes: |
| files.update(crash.binaries) |
| core = self._find_coredump_file(crash) |
| if core: |
| files.add(core) |
| else: |
| missing.append(crash) |
| if self._output_directory is not None and self._is_shard(): |
| print( |
| "INFO: Moving collected dumps and binaries into output directory\n" |
| "INFO: They will be uploaded to isolate server. Look for \"isolated" |
| " out\" under the failed step on the build page.\n" |
| "INFO: For more information see runtime/docs/infra/coredumps.md" |
| ) |
| self._move(files) |
| else: |
| print( |
| "INFO: Uploading collected dumps and binaries into Cloud Storage\n" |
| "INFO: Use `gsutil.py cp from-url to-path` to download them.\n" |
| "INFO: For more information see runtime/docs/infra/coredumps.md" |
| ) |
| self._upload(files) |
| |
| if missing: |
| self._report_missing_crashes(missing, throw=False) |
| |
| # todo(athom): move the logic to decide where to copy core dumps into the recipes. |
| def _is_shard(self): |
| return 'BUILDBOT_BUILDERNAME' not in os.environ |
| |
| def _report_missing_crashes(self, missing, throw=False): |
| missing_as_string = ', '.join([str(c) for c in missing]) |
| other_files = list(glob.glob(os.path.join(self._search_dir, '*'))) |
| sys.stderr.write( |
| "Could not find crash dumps for '{}' in search directory '{}'.\n" |
| "Existing files which *did not* match the pattern inside the search " |
| "directory are are:\n {}\n".format(missing_as_string, |
| self._search_dir, |
| '\n '.join(other_files))) |
| # TODO: Figure out why windows coredump generation does not work. |
| # See http://dartbug.com/36469 |
| if throw and GuessOS() != 'win32': |
| raise Exception( |
| 'Missing crash dumps for: {}'.format(missing_as_string)) |
| |
| def _get_file_name(self, file): |
| # Sanitize the name: actual cores follow 'core.%d' pattern, crashed |
| # binaries are copied next to cores and named |
| # 'binary.<mode>_<arch>_<binary_name>'. |
| # This should match the code in testing/dart/test_progress.dart |
| name = os.path.basename(file) |
| (prefix, suffix) = name.split('.', 1) |
| is_binary = prefix == 'binary' |
| if is_binary: |
| (mode, arch, binary_name) = suffix.split('_', 2) |
| name = binary_name |
| return (name, is_binary) |
| |
| def _move(self, files): |
| for file in files: |
| print('+++ Moving {} to output_directory ({})'.format( |
| file, self._output_directory)) |
| (name, is_binary) = self._get_file_name(file) |
| destination = os.path.join(self._output_directory, name) |
| shutil.move(file, destination) |
| if is_binary and os.path.exists(file + '.pdb'): |
| # Also move a PDB file if there is one. |
| pdb = os.path.join(self._output_directory, name + '.pdb') |
| shutil.move(file + '.pdb', pdb) |
| |
| def _tar(self, file): |
| (name, is_binary) = self._get_file_name(file) |
| tarname = '{}.tar.gz'.format(name) |
| |
| # Compress the file. |
| tar = tarfile.open(tarname, mode='w:gz') |
| tar.add(file, arcname=name) |
| if is_binary and os.path.exists(file + '.pdb'): |
| # Also add a PDB file if there is one. |
| tar.add(file + '.pdb', arcname=name + '.pdb') |
| tar.close() |
| return tarname |
| |
| def _upload(self, files): |
| bot_utils = GetBotUtils() |
| gsutil = bot_utils.GSUtil() |
| storage_path = '{}/{}/'.format(self._bucket, uuid.uuid4()) |
| gs_prefix = 'gs://{}'.format(storage_path) |
| http_prefix = 'https://storage.cloud.google.com/{}'.format(storage_path) |
| |
| print('\n--- Uploading into {} ({}) ---'.format(gs_prefix, http_prefix)) |
| for file in files: |
| tarname = self._tar(file) |
| |
| # Remove / from absolute path to not have // in gs path. |
| gs_url = '{}{}'.format(gs_prefix, tarname) |
| http_url = '{}{}'.format(http_prefix, tarname) |
| |
| try: |
| gsutil.upload(tarname, gs_url) |
| print('+++ Uploaded {} ({})'.format(gs_url, http_url)) |
| except Exception as error: |
| print('!!! Failed to upload {}, error: {}'.format( |
| tarname, error)) |
| |
| TryUnlink(tarname) |
| |
| print('--- Done ---\n') |
| |
| def _find_all_coredumps(self): |
| """Return coredumps that were recorded (if supported by the platform). |
| This method will be overridden by concrete platform specific implementations. |
| """ |
| return [] |
| |
| def _find_unexpected_crashes(self): |
| """Load coredumps file. Each line has the following format: |
| |
| test-name,pid,binary-file1,binary-file2,... |
| """ |
| try: |
| with open(BaseCoreDumpArchiver._UNEXPECTED_CRASHES_FILE) as f: |
| return [ |
| UnexpectedCrash(*ln.strip('\n').split(',')) |
| for ln in f.readlines() |
| ] |
| except: |
| return [] |
| |
| def _cleanup(self): |
| found = False |
| if os.path.exists(BaseCoreDumpArchiver._UNEXPECTED_CRASHES_FILE): |
| os.unlink(BaseCoreDumpArchiver._UNEXPECTED_CRASHES_FILE) |
| found = True |
| for binary in glob.glob(os.path.join(self._binaries_dir, 'binary.*')): |
| found = True |
| TryUnlink(binary) |
| |
| return found |
| |
| |
| class PosixCoreDumpArchiver(BaseCoreDumpArchiver): |
| |
| def __init__(self, search_dir, output_directory): |
| super(PosixCoreDumpArchiver, self).__init__(search_dir, |
| output_directory) |
| |
| def _cleanup(self): |
| found = super(PosixCoreDumpArchiver, self)._cleanup() |
| for core in glob.glob(os.path.join(self._search_dir, 'core.*')): |
| found = True |
| TryUnlink(core) |
| return found |
| |
| def _find_coredump_file(self, crash): |
| core_filename = os.path.join(self._search_dir, |
| 'core.{}'.format(crash.pid)) |
| if os.path.exists(core_filename): |
| return core_filename |
| |
| |
| class LinuxCoreDumpArchiver(PosixCoreDumpArchiver): |
| |
| def __init__(self, output_directory): |
| super(LinuxCoreDumpArchiver, self).__init__(os.getcwd(), |
| output_directory) |
| |
| |
| class MacOSCoreDumpArchiver(PosixCoreDumpArchiver): |
| |
| def __init__(self, output_directory): |
| super(MacOSCoreDumpArchiver, self).__init__('/cores', output_directory) |
| |
| |
| class WindowsCoreDumpArchiver(BaseCoreDumpArchiver): |
| |
| def __init__(self, output_directory): |
| super(WindowsCoreDumpArchiver, self).__init__( |
| WindowsCoreDumpEnabler.DUMPS_FOLDER, output_directory) |
| self._dumps_by_pid = None |
| |
| # Find CDB.exe in the win_toolchain that we are using. |
| def _find_cdb(self): |
| win_toolchain_json_path = os.path.join(DART_DIR, 'build', |
| 'win_toolchain.json') |
| if not os.path.exists(win_toolchain_json_path): |
| return None |
| |
| with open(win_toolchain_json_path, 'r') as f: |
| win_toolchain_info = json.loads(f.read()) |
| |
| win_sdk_path = win_toolchain_info['win_sdk'] |
| |
| # We assume that we are running on 64-bit Windows. |
| # Note: x64 CDB can work with both X64 and IA32 dumps. |
| cdb_path = os.path.join(win_sdk_path, 'Debuggers', 'x64', 'cdb.exe') |
| if not os.path.exists(cdb_path): |
| return None |
| |
| return cdb_path |
| |
| CDBG_PROMPT_RE = re.compile(r'^\d+:\d+>') |
| |
| def _dump_all_stacks(self): |
| # On Windows due to crashpad integration crashes do not produce any |
| # stacktraces. Dump stack traces from dumps Crashpad collected using |
| # CDB (if available). |
| cdb_path = self._find_cdb() |
| if cdb_path is None: |
| return |
| |
| dumps = self._find_all_coredumps() |
| if not dumps: |
| return |
| |
| print('### Collected {} crash dumps'.format(len(dumps))) |
| for dump in dumps: |
| print() |
| print('### Dumping stacks from {} using CDB'.format(dump)) |
| cdb_output = subprocess.check_output( |
| '"{}" -z "{}" -kqm -c "!uniqstack -b -v -p;qd"'.format( |
| cdb_path, dump), |
| stderr=subprocess.STDOUT) |
| # Extract output of uniqstack from the whole output of CDB. |
| output = False |
| for line in cdb_output.split('\n'): |
| if re.match(WindowsCoreDumpArchiver.CDBG_PROMPT_RE, line): |
| output = True |
| elif line.startswith('quit:'): |
| break |
| elif output: |
| print(line) |
| print() |
| print('#############################################') |
| print() |
| |
| def __exit__(self, *args): |
| try: |
| self._dump_all_stacks() |
| except Exception as error: |
| print('ERROR: Unable to dump stacks from dumps: {}'.format(error)) |
| |
| super(WindowsCoreDumpArchiver, self).__exit__(*args) |
| |
| def _cleanup(self): |
| found = super(WindowsCoreDumpArchiver, self)._cleanup() |
| for core in glob.glob(os.path.join(self._search_dir, '*')): |
| found = True |
| TryUnlink(core) |
| return found |
| |
| def _find_all_coredumps(self): |
| pattern = os.path.join(self._search_dir, '*.dmp') |
| return [core_filename for core_filename in glob.glob(pattern)] |
| |
| def _find_coredump_file(self, crash): |
| if self._dumps_by_pid is None: |
| # If this function is invoked the first time then look through the directory |
| # that contains crashes for all dump files and collect pid -> filename |
| # mapping. |
| self._dumps_by_pid = {} |
| minidump = GetMinidumpUtils() |
| pattern = os.path.join(self._search_dir, '*.dmp') |
| for core_filename in glob.glob(pattern): |
| pid = minidump.GetProcessIdFromDump(core_filename) |
| if pid != -1: |
| self._dumps_by_pid[str(pid)] = core_filename |
| if crash.pid in self._dumps_by_pid: |
| return self._dumps_by_pid[crash.pid] |
| |
| def _report_missing_crashes(self, missing, throw=False): |
| # Let's only print the debugging information and not throw. We'll do more |
| # validation for werfault.exe and throw afterwards. |
| super(WindowsCoreDumpArchiver, self)._report_missing_crashes( |
| missing, throw=False) |
| |
| if throw: |
| missing_as_string = ', '.join([str(c) for c in missing]) |
| raise Exception( |
| 'Missing crash dumps for: {}'.format(missing_as_string)) |
| |
| |
| class IncreasedNumberOfFileDescriptors(object): |
| |
| def __init__(self, nofiles): |
| self._old_limits = None |
| self._limits = (nofiles, nofiles) |
| |
| def __enter__(self): |
| self._old_limits = resource.getrlimit(resource.RLIMIT_NOFILE) |
| resource.setrlimit(resource.RLIMIT_NOFILE, self._limits) |
| |
| def __exit__(self, *_): |
| resource.setrlimit(resource.RLIMIT_CORE, self._old_limits) |
| |
| |
| @contextlib.contextmanager |
| def NooptContextManager(): |
| yield |
| |
| |
| def CoreDumpArchiver(args): |
| enabled = '--copy-coredumps' in args |
| prefix = '--output-directory=' |
| output_directory = next( |
| (arg[len(prefix):] for arg in args if arg.startswith(prefix)), None) |
| |
| if not enabled: |
| return (NooptContextManager(),) |
| |
| osname = GuessOS() |
| if osname == 'linux': |
| return (LinuxCoreDumpEnabler(), LinuxCoreDumpArchiver(output_directory)) |
| elif osname == 'macos': |
| return (PosixCoreDumpEnabler(), MacOSCoreDumpArchiver(output_directory)) |
| elif osname == 'win32': |
| return (WindowsCoreDumpEnabler(), |
| WindowsCoreDumpArchiver(output_directory)) |
| |
| # We don't have support for MacOS yet. |
| return (NooptContextManager(),) |
| |
| |
| def FileDescriptorLimitIncreaser(): |
| osname = GuessOS() |
| if osname == 'macos': |
| return IncreasedNumberOfFileDescriptors(nofiles=10000) |
| |
| assert osname in ('linux', 'win32') |
| # We don't have support for MacOS yet. |
| return NooptContextManager() |
| |
| |
| def Main(): |
| print('GuessOS() -> ', GuessOS()) |
| print('GuessArchitecture() -> ', GuessArchitecture()) |
| print('GuessCpus() -> ', GuessCpus()) |
| print('IsWindows() -> ', IsWindows()) |
| print('GetGitRevision() -> ', GetGitRevision()) |
| print('GetGitTimestamp() -> ', GetGitTimestamp()) |
| print('ReadVersionFile() -> ', ReadVersionFile()) |
| |
| |
| if __name__ == '__main__': |
| Main() |