| #!/usr/bin/env python |
| |
| # Copyright 2012 The Goma Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # TODO: remove GOMA_COMPILER_PROXY_PORT from code. |
| # it could be 8089, 8090, ... actually. |
| """A Script to manage compiler_proxy. |
| |
| It starts/stops compiler_proxy.exe or compiler_proxy. |
| """ |
| |
| from __future__ import print_function |
| |
| |
| |
| |
| import collections |
| import copy |
| import datetime |
| import glob |
| import gzip |
| import hashlib |
| import io |
| import json |
| import os |
| import random |
| import re |
| import shutil |
| import socket |
| import string |
| import subprocess |
| import sys |
| import tarfile |
| import tempfile |
| import time |
| try: |
| import urllib.parse, urllib.request |
| URLOPEN = urllib.request.urlopen |
| URLOPEN2 = urllib.request.urlopen |
| URLREQUEST = urllib.request.Request |
| except ImportError: |
| import urllib |
| import urllib2 |
| URLOPEN = urllib.urlopen |
| URLOPEN2 = urllib2.urlopen |
| URLREQUEST = urllib2.Request |
| import zipfile |
| |
| SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) |
| _TRUE_PATTERN = re.compile(r'^([tTyY]|1)') |
| _DEFAULT_ENV = [ |
| ('USE_SSL', 'true'), |
| ('PING_TIMEOUT_SEC', '60'), |
| ('LOG_CLEAN_INTERVAL', str(24 * 60 * 60)), |
| ] |
| _DEFAULT_NO_SSL_ENV = [ |
| ('STUBBY_PROXY_PORT', '80'), |
| ] |
| _MAX_COOLDOWN_WAIT = 10 # seconds to wait for compiler_proxy to shutdown. |
| _COOLDOWN_SLEEP = 1 # seconds to each wait for compiler_proxy to shutdown. |
| _CRASH_DUMP_DIR = 'goma_crash' |
| _CACHE_DIR = 'goma_cache' |
| _PRODUCT_NAME = 'Goma' # product name used for crash report. |
| _DUMP_FILE_SUFFIX = '.dmp' |
| _CHECKSUM_FILE = 'sha256.json' |
| |
| _TIMESTAMP_PATTERN = re.compile('(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})') |
| _TIMESTAMP_FORMAT = '%Y/%m/%d %H:%M:%S' |
| |
| _CRASH_SERVER = 'https://clients2.google.com/cr/report' |
| _STAGING_CRASH_SERVER = 'https://clients2.google.com/cr/staging_report' |
| |
| def _IsGomaFlagTrue(flag_name, default=False): |
| """Return true when the given flag is true. |
| |
| Note: |
| Implementation is based on client/env_flags.h. |
| Any values that do not match _TRUE_PATTERN are False. |
| |
| Args: |
| flag_name: name of a GOMA flag without GOMA_ prefix. |
| default: default return value if the flag is not set. |
| |
| Returns: |
| True if the flag is true. Otherwise False. |
| """ |
| flag_value = os.environ.get('GOMA_%s' % flag_name, '') |
| if not flag_value: |
| return default |
| return bool(_TRUE_PATTERN.search(flag_value)) |
| |
| |
| def _SetGomaFlagDefaultValueIfEmpty(flag_name, default_value): |
| """Set default value to the given flag if it is not set. |
| |
| Args: |
| flag_name: name of a GOMA flag without GOMA_ prefix. |
| default_value: default value to be set if the flag is not set. |
| """ |
| full_flag_name = 'GOMA_%s' % flag_name |
| if full_flag_name not in os.environ: |
| os.environ[full_flag_name] = default_value |
| |
| |
| def _ParseManifestContents(manifest): |
| """Parse contents of MANIFEST into a dictionary. |
| |
| Args: |
| manifest: a string of manifest to be parsed. |
| |
| Returns: |
| The dictionary of key and values in string. |
| """ |
| output = {} |
| for line in manifest.splitlines(): |
| pair = line.strip().split('=', 1) |
| if len(pair) == 2: |
| output[pair[0].strip()] = pair[1].strip() |
| return output |
| |
| |
| def _IsBadVersion(cur_ver, bad_vers): |
| """Check cur_ver is in bad_vers. |
| |
| Args: |
| cur_ver: current version number. |
| bad_vers: a string for bad version, '|' separated. |
| Returns: |
| True if cur_ver is in bad_vers. |
| """ |
| for ver in bad_vers.split('|'): |
| if str(cur_ver) == ver: |
| return True |
| return False |
| |
| |
| def _ShouldUpdate(cur_ver, next_ver, bad_vers): |
| """Check to update from cur_ver to next_ver. |
| |
| Basically, update to newer version (i.e. cur_ver < next_ver). |
| If cur_ver is the same as next_ver, then should not update (because |
| it is already up-to-date). |
| If cur_ver is listed in bad_vers, then |
| should update to next_ver even if cur_ver > next_ver. |
| |
| Args: |
| cur_ver: current version number |
| next_ver: next version number |
| bad_vers: a string for bad versions, '|' separated. |
| Returns: |
| True to update cur_ver to next_ver. False otherwise. |
| """ |
| if cur_ver < next_ver: |
| return True |
| if cur_ver == next_ver: |
| return False |
| return _IsBadVersion(cur_ver, bad_vers) |
| |
| |
| def _ParseSpaceSeparatedValues(data): |
| """Parses space separated values. |
| |
| This function assumes that 1st line is a list of labels. |
| |
| e.g. If data is like this: |
| | COMMAND PID |
| | bash 1 |
| | tcsh 2 |
| This function returns: |
| | [{'COMMAND': 'bash', 'PID': '1'}, {'COMMAND': 'tcsh', 'PID': '2'}] |
| |
| Args: |
| data: space separated values to be parsed. |
| |
| Returns: |
| a list of dictionaries parsed from data. |
| """ |
| # TODO: remove this if I will not use this on Windows. |
| label = None |
| contents = [] |
| for line in data.splitlines(): |
| entries = line.split() |
| if not entries: # skip empty line. |
| continue |
| |
| if not label: # 1st line. |
| label = entries |
| else: |
| contents.append(dict(list(zip(label, entries)))) |
| return contents |
| |
| |
| def _ParseLsof(data): |
| """Parse lsof. |
| |
| Expected inputs are results of: |
| - lsof -F pun <filename> |
| e.g. |
| p1234 |
| u5678 |
| f9 |
| - lsof -F pun -p <pid> |
| e.g. |
| p1234 |
| u5678 |
| fcwd |
| n/ |
| fmem |
| n/lib/ld.so |
| (f and n repeats, which should be owned by pid:1234 & uid:5678) |
| - lsof -F pun -P -i tcp:<port> |
| e.g. |
| p1234 |
| u5678 |
| f9 |
| nlocalhost:1112 |
| |
| Although this function might only be used on Posix environment, I put this |
| here for ease of testing. |
| |
| This function returns: |
| | [{'uid': 1L, 'pid': 2L}] |
| or |
| | [{'uid': 1L, 'pid': 2L, 'name': '/tmp/goma.ipc'}] |
| or |
| | [{'uid': 1L, 'pid': 2L, 'name': 'localhost:8088'}] |
| |
| Args: |
| data: result of lsof. |
| |
| Returns: |
| a list of dictionaries parsed from data. |
| """ |
| pid = None |
| uid = None |
| contents = [] |
| for line in data.splitlines(): |
| if not line: # skip empty line. |
| continue |
| |
| code = line[0] |
| value = line[1:] |
| |
| if code == 'p': |
| pid = int(value) |
| elif code == 'u': |
| uid = int(value) |
| elif code == 'n': |
| if uid is None or pid is None: |
| raise Error('failed to parse lsof result: %s' % data) |
| # Omit type=STREAM at the end. |
| TYPE_STREAM = ' type=STREAM' |
| if value.endswith(TYPE_STREAM): |
| value = value[0:-len(TYPE_STREAM)] |
| contents.append({'name': value, |
| 'uid': uid, |
| 'pid': pid}) |
| return contents |
| |
| |
| def _GetEnvMatchedCondition(candidates, condition, default_value): |
| """Returns environmental variable that matched the condition. |
| |
| Args: |
| candidates: a list of string to specify environmental variables. |
| condition: a condition to decide which value to return. |
| default_value: a string to be returned if no candidates matched. |
| |
| Returns: |
| a string of enviromnental variable's value that matched the condition. |
| Otherwise, default_value will be returned. |
| """ |
| for candidate in candidates: |
| value = os.environ.get(candidate, '') |
| if value and condition(value): |
| return value |
| return default_value |
| |
| |
| def _GetTempDirectory(): |
| """Get temp directory. |
| |
| Returns: |
| a directory name. |
| """ |
| candidates = ['TMPDIR', 'TMP'] |
| return _GetEnvMatchedCondition(candidates, os.path.isdir, '/tmp') |
| |
| |
| def _GetLogDirectory(): |
| """Get directory where log exists. |
| |
| Returns: |
| a directory name. |
| """ |
| candidates = ['GLOG_log_dir', 'TEST_TMPDIR', 'TMPDIR', 'TMP'] |
| return _GetEnvMatchedCondition(candidates, os.path.isdir, '/tmp') |
| |
| |
| def _GetUsernameEnv(): |
| """Get user name. |
| |
| Returns: |
| an user name that runs this script. |
| """ |
| candidates = ['SUDO_USER', 'USERNAME', 'USER', 'LOGNAME'] |
| return _GetEnvMatchedCondition(candidates, |
| lambda x: x != 'root', |
| '') |
| |
| |
| def _GetHostname(): |
| """Gets hostname. |
| |
| Returns: |
| a hostname of the machine running this script. |
| """ |
| return socket.gethostname() |
| |
| |
| def _FindCommandInPath(command, find_subdir_rule=os.path.join): |
| """Find command in the PATH. |
| |
| Args: |
| command: a string of command name to find out. |
| find_subdir_rule: a function to explore sub directory. |
| |
| Returns: |
| a string of a full path name if the command is found. Otherwise, None. |
| """ |
| for directory in os.environ['PATH'].split(os.path.pathsep): |
| fullpath = find_subdir_rule(directory, command) |
| if fullpath and os.path.isfile(fullpath) and os.access(fullpath, os.X_OK): |
| return fullpath |
| return None |
| |
| |
| def _ParseFlagz(flagz): |
| """Returns a dictionary of user-configured flagz. |
| |
| Note that the dictionary will not contain auto configured flags. |
| |
| Args: |
| flagz: a string returned by compiler_proxy's /flagz. |
| |
| Returns: |
| a dictionary of user-configured flags. |
| """ |
| envs = {} |
| for line in flagz.splitlines(): |
| line = line.strip() |
| if line.endswith('(auto configured)'): |
| continue |
| pair = line.split('=', 1) |
| if len(pair) == 2: |
| envs[pair[0].strip()] = pair[1].strip() |
| return envs |
| |
| |
| def _IsGomaFlagUpdated(envs): |
| """Returns true if environment is updated from the argument. |
| |
| Note: caller MUST NOT set environ after call of this method. |
| Otherwise, this function may always return true. |
| |
| Args: |
| a dictionary of environment to check. e.g. { |
| 'GOMA_USE_SSL': 'true', |
| } |
| |
| Returns: |
| True if one of values is different from given one. |
| """ |
| for key, original in envs.items(): |
| new = os.environ.get(key) |
| if new != original: |
| return True |
| for key, value in os.environ.items(): |
| if key.startswith('GOMA_'): |
| if value != envs.get(key): |
| return True |
| return False |
| |
| |
| def _CalculateChecksum(filename): |
| """Calculate SHA256 checksum of given file. |
| |
| Args: |
| filename: a string filename to calculate checksum. |
| |
| Returns: |
| hexdigest string of file contents. |
| """ |
| with open(filename, 'rb') as f: |
| return hashlib.sha256(f.read()).hexdigest() |
| |
| |
| def _GetLogFileTimestamp(glog_log): |
| """Returns timestamp when the given glog log was created. |
| |
| Args: |
| glog_log: a filename of a google-glog log. |
| |
| Returns: |
| datetime instance when the logfile was created. |
| Or, returns None if not a glog file. |
| |
| Raises: |
| IOError if this function cannot open glog_log. |
| """ |
| with open(glog_log) as f: |
| matched = _TIMESTAMP_PATTERN.search(f.readline()) |
| if matched: |
| return datetime.datetime.strptime(matched.group(1), _TIMESTAMP_FORMAT) |
| return None |
| |
| |
| class ConfigError(Exception): |
| """Raises when an error found in configurations.""" |
| |
| |
| class Error(Exception): |
| """Raises when an error found in the system.""" |
| |
| |
| class PopenWithCheck(subprocess.Popen): |
| """subprocess.Popen with automatic exit status check on communicate.""" |
| |
| def communicate(self, input=None): |
| # I do not think argument name |input| is good but this is from the original |
| # communicate method. |
| # pylint: disable=W0622 |
| (stdout, stderr) = super(PopenWithCheck, self).communicate(input) |
| if self.returncode is None or self.returncode != 0: |
| if stdout or stderr: |
| raise Error('Error(%s): %s%s' % (self.returncode, stdout, stderr)) |
| else: |
| raise Error('failed to execute subprocess return=%s' % self.returncode) |
| return (stdout, stderr) |
| |
| |
| class GomaDriver(object): |
| """Driver of Goma control.""" |
| |
| def __init__(self, env, backend): |
| """Initialize GomaDriver. |
| |
| Args: |
| env: an instance of GomaEnv subclass. |
| backend: an instance of GomaBackend subclass. |
| """ |
| self._env = env |
| self._backend = backend |
| self._latest_package_dir = 'latest' |
| self._action_mappings = { |
| 'audit': self._CheckAudit, |
| 'ensure_start': self._EnsureStartCompilerProxy, |
| 'ensure_stop': self._EnsureStopCompilerProxy, |
| 'histogram': self._PrintHistogram, |
| 'jsonstatus': self._PrintJsonStatus, |
| 'report': self._Report, |
| 'restart': self._RestartCompilerProxy, |
| 'showflags': self._PrintFlags, |
| 'start': self._StartCompilerProxy, |
| 'stat': self._PrintStatistics, |
| 'status': self._CheckStatus, |
| 'stop': self._ShutdownCompilerProxy, |
| } |
| self._version = 0 |
| self._manifest = {} |
| self._args = [] |
| self._ReadManifest() |
| self._compiler_proxy_running = None |
| |
| def _ReadManifest(self): |
| """Reads MANIFEST file. |
| """ |
| self._manifest = self._env.ReadManifest() |
| if 'VERSION' in self._manifest: |
| self._version = int(self._manifest['VERSION']) |
| if 'GOMA_API_KEY_FILE' in self._manifest: |
| sys.stderr.write('WARNING: GOMA_API_KEY_FILE is deprecated\n') |
| |
| def _UpdateManifest(self): |
| """Write self._manifest to in MANIFEST.""" |
| self._env.WriteManifest(self._manifest) |
| |
| def _ValidFiles(self, files): |
| """Validate files.""" |
| for f in files: |
| filename = os.path.join(self._latest_package_dir, f) |
| if not self._env.IsValidMagic(filename): |
| print('%s is broken.' % filename) |
| return False |
| return True |
| |
| def _Pull(self): |
| """Download the latest package to goma_dir/latest.""" |
| latest_version, bad_version = self._GetLatestVersion() |
| files_to_download = ['MANIFEST', self._env.GetPackageName()] |
| if ((_ShouldUpdate(self._DownloadedVersion(), latest_version, |
| bad_version) or |
| not self._ValidFiles(files_to_download)) and |
| _ShouldUpdate(self._version, latest_version, bad_version)): |
| self._env.RemoveDirectory(self._latest_package_dir) |
| self._env.MakeDirectory(self._latest_package_dir) |
| base_url = self._backend.GetDownloadBaseUrl() |
| for f in files_to_download: |
| url = '%s/%s' % (base_url, f) |
| destination = os.path.join(self._latest_package_dir, f) |
| print('Downloading %s' % url) |
| self._env.HttpDownload(url, |
| rewrite_url=self._backend.RewriteRequest, |
| headers=self._backend.GetHeaders(), |
| destination_file=destination) |
| manifest = self._env.ReadManifest(self._latest_package_dir) |
| manifest['PLATFORM'] = self._env.GetPlatform() |
| self._env.WriteManifest(manifest, self._latest_package_dir) |
| else: |
| print('Downloaded package is already the latest version.') |
| |
| # update the timestamp of MANIFEST in self._latest_package_dir |
| # to skip unnecessary download in ensure_start if the file is valid. |
| if self._env.IsValidManifest(self._latest_package_dir): |
| manifest = self._env.ReadManifest(directory=self._latest_package_dir) |
| self._env.WriteManifest(manifest, directory=self._latest_package_dir) |
| |
| def _GetRunningCompilerProxyVersion(self): |
| versionz = self._env.ControlCompilerProxy('/versionz', check_running=False) |
| if versionz['status']: |
| return versionz['message'].strip() |
| return None |
| |
| def _GetDiskCompilerProxyVersion(self): |
| return self._env.GetCompilerProxyVersion().replace('GOMA version', |
| '').strip() |
| |
| def _GetCompilerProxyHealthz(self): |
| """Returns compiler proxy healthz message.""" |
| healthz = self._env.ControlCompilerProxy('/healthz', check_running=False) |
| if healthz['status']: |
| return healthz['message'].split()[0].strip() |
| return 'unavailable /healthz' |
| |
| def _IsCompilerProxySilentlyUpdated(self): |
| """Returns True if compiler_proxy is different from running version.""" |
| disk_version = self._GetDiskCompilerProxyVersion() |
| running_version = self._GetRunningCompilerProxyVersion() |
| if running_version: |
| return running_version != disk_version |
| return False |
| |
| def _IsGomaFlagUpdated(self): |
| flagz = self._env.ControlCompilerProxy('/flagz', check_running=False) |
| if flagz['status']: |
| return _IsGomaFlagUpdated(_ParseFlagz(flagz['message'].strip())) |
| return False |
| |
| def _GenericStartCompilerProxy(self, ensure=False): |
| self._env.CheckConfig() |
| if self._compiler_proxy_running is None: |
| self._compiler_proxy_running = self._env.CompilerProxyRunning() |
| if (not ensure and self._env.MayUsingDefaultIPCPort() and |
| self._compiler_proxy_running): |
| self._KillStakeholders() |
| self._compiler_proxy_running = False |
| |
| can_auto_update = self._env.CanAutoUpdate() |
| if can_auto_update: |
| bad_version = '' |
| |
| if (self._env.ReadManifest(self._latest_package_dir) and |
| self._env.IsManifestModifiedRecently(self._latest_package_dir)): |
| print('Auto update is skipped' |
| ' because %s/MANIFEST was updated recently.' % |
| self._latest_package_dir) |
| latest_version = self._version |
| else: |
| latest_version, bad_version = self._GetLatestVersion() |
| do_update = False |
| if self._version < latest_version: |
| print('new goma client found (VERSION=%d).' % latest_version) |
| do_update = True |
| if _IsBadVersion(self._version, bad_version): |
| print('your version (VERSION=%d) is marked as bad version (%s)' % ( |
| self._version, bad_version)) |
| do_update = True |
| if do_update: |
| print('Updating...') |
| self._env.AutoUpdate() |
| # AutoUpdate may change running status. |
| self._compiler_proxy_running = self._env.CompilerProxyRunning() |
| self._ReadManifest() |
| |
| if 'VERSION' in self._manifest: |
| print('Using goma VERSION=%s (%s)' % ( |
| self._manifest['VERSION'], |
| 'latest' if can_auto_update else 'no_auto_update')) |
| disk_version = self._GetDiskCompilerProxyVersion() |
| print('GOMA version %s' % disk_version) |
| if ensure and self._compiler_proxy_running: |
| healthz = self._GetCompilerProxyHealthz() |
| if healthz != 'ok': |
| print('goma is not in healthy state: %s' % healthz) |
| updated = self._IsCompilerProxySilentlyUpdated() |
| flag_updated = self._IsGomaFlagUpdated() |
| if flag_updated: |
| print('flagz is updated from the previous time.') |
| if healthz != 'ok' or updated or flag_updated: |
| self._ShutdownCompilerProxy() |
| if not self._WaitCooldown(): |
| self._KillStakeholders() |
| self._compiler_proxy_running = False |
| |
| if ensure and self._compiler_proxy_running: |
| print() |
| print('goma is already running.') |
| print() |
| return |
| |
| # AutoUpdate may restart compiler proxy. |
| if not self._compiler_proxy_running: |
| self._env.ExecCompilerProxy() |
| self._compiler_proxy_running = True |
| |
| if self._GetStatus(): |
| running_version = self._GetRunningCompilerProxyVersion() |
| if running_version != disk_version: |
| print('Updated GOMA version %s' % running_version) |
| print() |
| print('Now goma is ready!') |
| print() |
| return |
| else: |
| sys.stderr.write('Failed to start compiler_proxy.\n') |
| try: |
| subprocess.check_call( |
| [sys.executable, |
| os.path.join(self._env.GetScriptDir(), 'goma_auth.py'), |
| 'info']) |
| sys.stderr.write('Temporary error? try again\n') |
| except subprocess.CalledProcessError: |
| pass |
| sys.exit(1) |
| |
| def _StartCompilerProxy(self): |
| self._GenericStartCompilerProxy(ensure=False) |
| |
| def _EnsureStartCompilerProxy(self): |
| self._GenericStartCompilerProxy(ensure=True) |
| |
| def _EnsureStopCompilerProxy(self): |
| self._ShutdownCompilerProxy() |
| if not self._WaitCooldown(): |
| self._KillStakeholders() |
| |
| def _CheckStatus(self): |
| status = self._GetStatus() |
| if not status: |
| sys.exit(1) |
| return |
| |
| def _GetStatus(self): |
| reply = self._env.ControlCompilerProxy('/healthz', need_pids=True) |
| print('compiler proxy (pid=%(pid)s) status: %(url)s %(message)s' % reply) |
| if reply['message'].startswith('error:'): |
| reply['status'] = False |
| return reply['status'] |
| |
| def _ShutdownCompilerProxy(self): |
| print('Killing compiler proxy.') |
| reply = self._env.ControlCompilerProxy('/quitquitquit') |
| print('compiler proxy status: %(url)s %(message)s' % reply) |
| |
| def _GetLatestVersion(self): |
| """Get latest version of goma. |
| |
| Returns: |
| A tuple of the version number and bad_version string from MANIFEST |
| |
| Raises: |
| Error: if failed to determine the latest version. |
| """ |
| try: |
| url = self._backend.GetDownloadBaseUrl() + '/MANIFEST' |
| except Error as ex: |
| oauth2_config_file = os.environ.get('GOMA_OAUTH2_CONFIG_FILE') |
| if (oauth2_config_file and |
| "not_initialized" in open(oauth2_config_file).read()): |
| return 0, "" |
| raise ex |
| contents = self._env.HttpDownload( |
| url, |
| rewrite_url=self._backend.RewriteRequest, |
| headers=self._backend.GetHeaders()) |
| manifest = _ParseManifestContents(contents) |
| if 'VERSION' in manifest: |
| return (int(manifest['VERSION']), manifest.get('bad_version', '')) |
| raise Error('Unable to determine the latest version. ' |
| 'Failed to download the latest valid MANIFEST ' |
| 'from the server.\n' |
| 'Response from server: %s' % contents) |
| |
| def _DownloadedVersion(self): |
| """Check version of already downloaded goma package. |
| |
| Returns: |
| The version as integer. |
| """ |
| version = 0 |
| try: |
| version = int(self._env.ReadManifest(self._latest_package_dir)['VERSION']) |
| except (KeyError, ValueError): |
| pass |
| return version |
| |
| def _WaitCooldown(self): |
| """Wait until compiler_proxy process have finished. |
| |
| This will give up after waiting _MAX_COOLDOWN_WAIT seconds. |
| It would return False, if other compiler_proxy is running on other IPC port. |
| |
| Returns: |
| True if compiler_proxy successfully cool down. Otherwise, False. |
| """ |
| if not self._env.CompilerProxyRunning(): |
| return True |
| print('Waiting for cool down...') |
| for cnt in range(_MAX_COOLDOWN_WAIT): |
| if not self._env.CompilerProxyRunning(): |
| break |
| print(_MAX_COOLDOWN_WAIT - cnt) |
| sys.stdout.flush() |
| time.sleep(_COOLDOWN_SLEEP) |
| else: |
| print('give up') |
| return False |
| print() |
| return True |
| |
| def _KillStakeholders(self): |
| """Kill and wait until its shutdown.""" |
| self._env.KillStakeholders() |
| if not self._WaitCooldown(): |
| print('Could not kill compiler_proxy.') |
| print('trying to kill it forcefully.') |
| self._env.KillStakeholders(force=True) |
| if not self._WaitCooldown(): |
| print('Could not kill compiler_proxy.') |
| print('Probably, somebody else also runs compiler_proxy.') |
| |
| def _UpdatePackage(self): |
| """Update or install latest package. |
| |
| We raise error immediately when there is anything wrong instead of |
| trying to do something smart. When things go wrong it can be disk |
| issues and it's better to have human intervention instead. |
| |
| Raises: |
| Error: if failed to install the package. |
| """ |
| update_dir = 'update' |
| self._env.RemoveDirectory(update_dir) |
| self._env.MakeDirectory(update_dir) |
| manifest = self._env.ReadManifest(self._latest_package_dir) |
| if not manifest or 'VERSION' not in manifest: |
| manifest_file = os.path.join(self._latest_package_dir, 'MANIFEST') |
| print('MANIFEST (%s) seems to be broken.' % manifest_file) |
| print('Going to remove MANIFEST.') |
| self._env.RemoveFile(manifest_file) |
| print('Please execute update again.') |
| raise Error('MANIFEST in downloaded version is broken.') |
| latest_version = int(manifest['VERSION']) |
| package_file = os.path.join(self._latest_package_dir, |
| self._env.GetPackageName()) |
| if not self._env.ExtractPackage(package_file, update_dir): |
| print('Package file (%s) seems to be broken.' % package_file) |
| print('Going to remove package_file.') |
| self._env.RemoveFile(package_file) |
| print('Please execute update again.') |
| raise Error('Failed to extract downloaded package') |
| if not self._Audit(update_dir=update_dir): |
| print('Failed to verify a file in package.') |
| print('Going to remove package_file and update_dir') |
| self._env.RemoveFile(package_file) |
| self._env.RemoveDirectory(update_dir) |
| raise Error('downloaded package is broken') |
| if self._env.IsGomaInstalledBefore(): |
| # This is an update rather than fresh install. |
| print('Stopping compiler_proxy ...') |
| self._ShutdownCompilerProxy() |
| if not self._WaitCooldown(): |
| self._KillStakeholders() |
| self._compiler_proxy_running = False |
| print('Updating package to %s ...' % self._env.GetScriptDir()) |
| if not self._env.InstallPackage(update_dir): |
| raise Error('Failed to install package') |
| self._version = latest_version |
| self._manifest.update(manifest) |
| self._UpdateManifest() |
| self._env.RemoveDirectory(update_dir) |
| |
| def _Update(self): |
| """Update goma binary to latest version.""" |
| latest_version, bad_version = self._GetLatestVersion() |
| if _ShouldUpdate(self._version, latest_version, bad_version): |
| self._Pull() |
| self._env.BackupCurrentPackage() |
| rollback = True |
| if self._compiler_proxy_running is None: |
| self._compiler_proxy_running = self._env.CompilerProxyRunning() |
| is_goma_running = self._compiler_proxy_running |
| try: |
| self._UpdatePackage() |
| rollback = False |
| finally: |
| if rollback: |
| print('Failed to update. Rollback...') |
| try: |
| self._env.RollbackUpdate() |
| except Error as inst: |
| print(inst) |
| if is_goma_running and not self._env.CompilerProxyRunning(): |
| print(self._env.GetCompilerProxyVersion()) |
| self._env.ExecCompilerProxy() |
| self._compiler_proxy_running = True |
| else: |
| print('Goma is already up-to-date.') |
| |
| def _RestartCompilerProxy(self): |
| if self._compiler_proxy_running is None: |
| self._compiler_proxy_running = self._env.CompilerProxyRunning() |
| if self._compiler_proxy_running: |
| self._ShutdownCompilerProxy() |
| if not self._WaitCooldown(): |
| self._KillStakeholders() |
| self._compiler_proxy_running = False |
| self._StartCompilerProxy() |
| |
| def _Fetch(self): |
| """Fetch requested goma package.""" |
| if len(self._args) < 2: |
| raise ConfigError('At least platform should be specified to fetch.') |
| platform = self._args[1] |
| pkg_name = _GetPackageName(platform) |
| if len(self._args) > 2: |
| outfile = os.path.join(os.getcwd(), self._args[2]) |
| else: |
| outfile = os.path.join(os.getcwd(), pkg_name) |
| url = '%s/%s' % (self._backend.GetDownloadBaseUrl(), pkg_name) |
| print('Downloading %s' % url) |
| self._env.HttpDownload(url, |
| rewrite_url=self._backend.RewriteRequest, |
| headers=self._backend.GetHeaders(), |
| destination_file=outfile) |
| |
| def _PrintLatestVersion(self): |
| """Print latest version on stdout.""" |
| latest_version, _ = self._GetLatestVersion() |
| print('VERSION=%d' % latest_version) |
| |
| def _PrintStatistics(self): |
| print(self._env.ControlCompilerProxy('/statz')['message']) |
| |
| def _PrintHistogram(self): |
| print(self._env.ControlCompilerProxy('/histogramz')['message']) |
| |
| def _PrintFlags(self): |
| flagz = self._env.ControlCompilerProxy('/flagz', check_running=False) |
| print(json.dumps(_ParseFlagz(flagz['message'].strip()))) |
| |
| def _PrintJsonStatus(self): |
| status = self._GetJsonStatus() |
| if len(self._args) > 1: |
| with open(self._args[1], 'w') as f: |
| f.write(status) |
| else: |
| print(status) |
| |
| def _CopyLatestInfoFile(self, command_name, dst): |
| """Copies latest *.INFO.* file to destination. |
| |
| Args: |
| command_name: command_name of *.INFO.* file to copy. |
| e.g. compiler_proxy. |
| dst: destination directory name. |
| """ |
| |
| infolog_path = self._env.FindLatestLogFile(command_name, 'INFO') |
| if infolog_path: |
| self._env.CopyFile(infolog_path, |
| os.path.join(dst, os.path.basename(infolog_path))) |
| else: |
| print('%s log was not found' % command_name) |
| |
| def _CopyGomaccInfoFiles(self, dst): |
| """Copies gomacc *.INFO.* file to destination. all gomacc logs after |
| latest compiler_proxy started. |
| |
| Args: |
| dst: destination directory name. |
| """ |
| |
| infolog_path = self._env.FindLatestLogFile('compiler_proxy', 'INFO') |
| if not infolog_path: |
| return |
| compiler_proxy_start_time = _GetLogFileTimestamp(infolog_path) |
| if not compiler_proxy_start_time: |
| print('compiler_proxy start time could not be inferred. ' + |
| 'gomacc logs won\'t be included.') |
| return |
| logs = glob.glob(os.path.join(_GetLogDirectory(), 'gomacc.*.INFO.*')) |
| for log in logs: |
| timestamp = _GetLogFileTimestamp(log) |
| if timestamp and timestamp > compiler_proxy_start_time: |
| self._env.CopyFile(log, os.path.join(dst, os.path.basename(log))) |
| |
| def _InferBuildDirectory(self): |
| """Infer latest build directory from compiler_proxy.INFO. |
| |
| This would work for chromium build. Not sure for android etc. |
| |
| Returns: |
| build directory if inferred. None otherwise. |
| """ |
| |
| infolog_path = self._env.FindLatestLogFile('compiler_proxy', 'INFO') |
| if not infolog_path: |
| print('compiler_proxy log was not found') |
| return None |
| |
| build_re = re.compile('.*Task:.*Start.* build_dir=(.*)') |
| |
| # List build_dir from compiler_proxy, and take only last 10 build dirs. |
| # TODO: move this code to GomaEnv to write tests. |
| build_dirs = collections.deque() |
| with open(infolog_path) as f: |
| for line in f.readlines(): |
| m = build_re.match(line) |
| if m: |
| build_dirs.append(m.group(1)) |
| if len(build_dirs) > 10: |
| build_dirs.popleft() |
| |
| if not build_dirs: |
| return None |
| |
| counter = collections.Counter(build_dirs) |
| for candidate, _ in counter.most_common(): |
| if os.path.exists(os.path.join(candidate, '.ninja_log')): |
| return candidate |
| return None |
| |
| def _Report(self): |
| tempdir = None |
| try: |
| tempdir = tempfile.mkdtemp() |
| |
| compiler_proxy_is_working = True |
| # Check compiler_proxy is working. |
| ret = self._env.ControlCompilerProxy('/healthz') |
| if ret.get('status', False): |
| print('compiler_proxy is working:') |
| else: |
| compiler_proxy_is_working = False |
| print('compiler_proxy is not working:') |
| print(' omit compiler_proxy stats') |
| |
| if compiler_proxy_is_working: |
| keys = ['compilerinfoz', 'histogramz', 'serverz', 'statz'] |
| for key in keys: |
| ret = self._env.ControlCompilerProxy('/' + key) |
| if not ret.get('status', False): |
| # Failed to get the message. compiler_proxy has died? |
| print(' failed to get %s: %s' % (key, ret['message'])) |
| continue |
| print(' include /%s' % key) |
| self._env.WriteFile(os.path.join(tempdir, key + '-output'), |
| ret['message']) |
| |
| self._CopyLatestInfoFile('compiler_proxy', tempdir) |
| self._CopyLatestInfoFile('compiler_proxy-subproc', tempdir) |
| self._CopyLatestInfoFile('goma_fetch', tempdir) |
| self._CopyGomaccInfoFiles(tempdir) |
| |
| build_dir = self._InferBuildDirectory() |
| if build_dir: |
| print('build directory is inferred as', build_dir) |
| src_ninja_log = os.path.join(build_dir, '.ninja_log') |
| if os.path.exists(src_ninja_log): |
| dst_ninja_log = os.path.join(tempdir, 'ninja_log') |
| self._env.CopyFile(src_ninja_log, dst_ninja_log) |
| print(' include ninja_log') |
| else: |
| print('build directory cannot be inferred:') |
| print(' omit ninja_log') |
| |
| output_filename = os.path.join(_GetTempDirectory(), 'goma-report.tgz') |
| self._env.MakeTgzFromDirectory(tempdir, output_filename) |
| |
| print() |
| print('A report file is successfully created:') |
| print(' ', output_filename) |
| finally: |
| if tempdir: |
| shutil.rmtree(tempdir, ignore_errors=True) |
| |
| def _GetJsonStatus(self): |
| reply = self._env.ControlCompilerProxy('/errorz') |
| if not reply['status']: |
| return json.dumps({ |
| 'notice': [ |
| { |
| 'version': 1, |
| 'compile_error': 'COMPILER_PROXY_UNREACHABLE', |
| }, |
| ]}) |
| return reply['message'] |
| |
| def _CheckAudit(self): |
| """Audit files in the goma client package. Exit failure on error.""" |
| if not self._Audit(): |
| sys.exit(1) |
| return |
| |
| def _Audit(self, update_dir=''): |
| """Audit files in the goma client package. |
| |
| If update_dir is an empty string, it audit current goma files. |
| |
| Args: |
| update_dir: directory containing goma files to verify. |
| |
| Returns: |
| False if failed to verify. |
| """ |
| cksums = self._env.LoadChecksum(update_dir=update_dir) |
| if not cksums: |
| print('No checksum could be loaded.') |
| return True |
| for filename, checksum in cksums.items(): |
| # TODO: remove following two lines after the next release. |
| # Windows checksum file has non-existing .pdb files. |
| if os.path.splitext(filename)[1] == '.pdb': |
| continue |
| digest = self._env.CalculateChecksum(filename, update_dir=update_dir) |
| if checksum != digest: |
| print('%s differs: %s != %s' % (filename, checksum, digest)) |
| return False |
| print('All files verified.') |
| return True |
| |
| def _UploadCrashDump(self): |
| """Upload crash dump if exists. |
| |
| Important Notice: |
| You should not too much trust version number shown in the crash report. |
| A version number shown in the crash report may different from what actually |
| created a crash dump. Since the version to be sent is collected when |
| goma_ctl.py starts up, it may pick the wrong version number if |
| compiler_proxy was silently changed without using goma_ctl.py. |
| """ |
| if self._env.IsProductionBinary(): |
| server_url = _CRASH_SERVER |
| else: |
| # Set this for testing crash dump upload feature. |
| server_url = os.environ.get('GOMACTL_CRASH_SERVER') |
| if server_url == 'staging': |
| server_url = _STAGING_CRASH_SERVER |
| |
| if not server_url: |
| # We do not upload crash dump made by the developer's binary. |
| return |
| |
| try: |
| (hash_value, timestamp) = self._GetDiskCompilerProxyVersion().split('@') |
| version = '%s.%s' % (hash_value[:8], timestamp) |
| except OSError: # means file not exist and we can ignore it. |
| version = '' |
| |
| if self._version and version: |
| version = 'ver %d %s' % (self._version, version) |
| |
| if _IsGomaFlagTrue('SEND_USER_INFO', default=True): |
| guid = '%s@%s' % (self._env.GetUsername(), _GetHostname()) |
| else: |
| guid = None |
| |
| for dump_file in self._env.GetCrashDumps(): |
| uploaded = False |
| # Upload crash dumps only when we know its version number. |
| if version: |
| sys.stderr.write( |
| 'Uploading crash dump: %s to %s\n' % (dump_file, server_url)) |
| try: |
| report_id = self._env.UploadCrashDump(server_url, _PRODUCT_NAME, |
| version, dump_file, guid=guid) |
| report_id_file = os.environ.get('GOMACTL_CRASH_REPORT_ID_FILE') |
| if report_id_file: |
| with open(report_id_file, 'w') as f: |
| f.write(report_id) |
| sys.stderr.write('Report Id: %s\n' % report_id) |
| uploaded = True |
| except Error as inst: |
| sys.stderr.write('Failed to upload crash dump: %s\n' % inst) |
| if uploaded or self._env.IsOldFile(dump_file): |
| try: |
| self._env.RemoveFile(dump_file) |
| except OSError as e: |
| print('failed to remove %s: %s.' % (dump_file, e)) |
| |
| def _CreateDirectory(self, dir_name, purpose, suppress_message=False): |
| info = { |
| 'purpose': purpose, |
| 'dir': dir_name, |
| } |
| if not self._env.IsDirectoryExist(info['dir']): |
| if not suppress_message: |
| sys.stderr.write('INFO: creating %(purpose)s dir (%(dir)s).\n' % info) |
| self._env.MakeDirectory(info['dir']) |
| else: |
| if not self._env.EnsureDirectoryOwnedByUser(info['dir']): |
| sys.stderr.write( |
| 'Error: %(purpose)s dir (%(dir)s) is not owned by you.\n' % info) |
| raise Error('%(purpose)s dir (%(dir)s) is not owned by you.' % info) |
| |
| def _CreateGomaTmpDirectory(self): |
| tmp_dir = self._env.GetGomaTmpDir() |
| self._CreateDirectory(tmp_dir, 'temp') |
| sys.stderr.write('using %s as tmpdir\n' % tmp_dir) |
| os.environ['GOMA_TMP_DIR'] = tmp_dir |
| |
| def _CreateCrashDumpDirectory(self): |
| self._CreateDirectory(self._env.GetCrashDumpDirectory(), 'crash dump', |
| suppress_message=True) |
| |
| def _CreateCacheDirectory(self): |
| self._CreateDirectory(self._env.GetCacheDirectory(), 'cache') |
| |
| def _Usage(self): |
| """Print usage.""" |
| program_name = self._env.GetGomaCtlScriptName() |
| print('Usage: %s <subcommand>, available subcommands are:' % program_name) |
| print(' audit audit goma client.') |
| print(' ensure_start start compiler proxy if it is not running') |
| print(' ensure_stop synchronous stop of compiler proxy') |
| print(' histogram show histogram') |
| print(' jsonstatus [outfile] show status report in JSON') |
| print(' report create a report file.') |
| print(' restart restart compiler proxy') |
| print(' start start compiler proxy') |
| print(' stat show statistics') |
| print(' status get compiler proxy status') |
| print(' stop asynchronous stop of compiler proxy') |
| |
| |
| def _DefaultAction(self): |
| if self._args and not self._args[0] in ('-h', '--help', 'help'): |
| print('unknown command: %s' % ' '.join(self._args)) |
| print() |
| self._Usage() |
| |
| def Dispatch(self, args): |
| """Parse and dispatch commands.""" |
| is_audit = args and (args[0] == 'audit') |
| if not is_audit: |
| # when audit, we don't want to run gomacc to detect temp directory, |
| # since gomacc might be binary for different platform. |
| self._CreateGomaTmpDirectory() |
| upload_crash_dump = False |
| if upload_crash_dump: |
| self._UploadCrashDump() |
| self._CreateCrashDumpDirectory() |
| self._CreateCacheDirectory() |
| self._args = args |
| if not args: |
| self._GetStatus() |
| else: |
| self._action_mappings.get(args[0], self._DefaultAction)() |
| |
| |
| class GomaEnv(object): |
| """Goma running environment.""" |
| # You must implement following protected variables in subclass. |
| _GOMACC = '' |
| _COMPILER_PROXY = '' |
| _GOMA_FETCH = '' |
| _COMPILER_PROXY_IDENTIFIER_ENV_NAME = '' |
| PLATFORM_CANDIDATES = [] |
| _DEFAULT_ENV = [] |
| _DEFAULT_SSL_ENV = [] |
| |
| def __init__(self, script_dir=SCRIPT_DIR): |
| self._dir = os.path.abspath(script_dir) |
| self._compiler_proxy_binary = os.environ.get( |
| 'GOMA_COMPILER_PROXY_BINARY', |
| os.path.join(self._dir, self._COMPILER_PROXY)) |
| self._goma_fetch = None |
| if os.path.exists(os.path.join(self._dir, self._GOMA_FETCH)): |
| self._goma_fetch = os.path.join(self._dir, self._GOMA_FETCH) |
| self._is_daemon_mode = False |
| self._gomacc_binary = os.path.join(self._dir, self._GOMACC) |
| self._manifest = self.ReadManifest(self._dir) |
| self._platform = self._manifest.get('PLATFORM', '') |
| # If manifest does not have PLATFORM, goma_ctl.py tries to get it from env. |
| # See: b/16274764 |
| if not self._platform: |
| self._platform = os.environ.get('PLATFORM', '') |
| self._version = self._manifest.get('VERSION', '') |
| self._time = time.time() |
| self._goma_params = None |
| self._gomacc_socket = None |
| self._gomacc_port = None |
| self._backup = None |
| self._goma_tmp_dir = None |
| # TODO: remove this in python3 |
| self._delete_tmp_dir = False |
| self._SetupEnviron() |
| |
| def __del__(self): |
| if self._delete_tmp_dir: |
| shutil.rmtree(self._goma_tmp_dir) |
| |
| # methods that may interfere with external environment. |
| def MayUsingDefaultIPCPort(self): |
| """Returns True if IPC port is not configured in environmental variables. |
| |
| If os.environ has self._COMPILER_PORT_IDENTIFIER_ENV_NAME, it may use |
| non-default IPC port. Otherwise, it would use default port. |
| |
| Returns: |
| True if os.environ does not have self._COMPILER_IDENTIFIER_ENV_NAME. |
| """ |
| return self._COMPILER_PROXY_IDENTIFIER_ENV_NAME not in os.environ |
| |
| def GetCompilerProxyVersion(self): |
| """Returns compiler proxy version.""" |
| return PopenWithCheck([self._compiler_proxy_binary, '--version'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT).communicate()[0].rstrip() |
| |
| def GetScriptDir(self): |
| return self._dir |
| |
| def IsManifestModifiedRecently(self, directory='', threshold=4*60*60): |
| manifest_path = os.path.join(self._dir, directory, 'MANIFEST') |
| return time.time() - os.stat(manifest_path).st_mtime < threshold |
| |
| def ReadManifest(self, directory=''): |
| """Read manifest from MANIFEST file in the directory. |
| |
| Args: |
| directory: a string of directory name to read the manifest file. |
| |
| Returns: |
| A dictionary of manifest if the manifest file exist. |
| Otherwise, an empty dictionary. |
| """ |
| manifest_path = os.path.join(self._dir, directory, 'MANIFEST') |
| if not os.path.isfile(manifest_path): |
| return {} |
| return _ParseManifestContents(open(manifest_path, 'r').read()) |
| |
| def WriteManifest(self, manifest, directory=''): |
| """Write manifest dictionary to MANIFEST file in the directory. |
| |
| Args: |
| manifest: a dictionary of the manifest. |
| directory: a string of directory name to write the manifest file. |
| """ |
| manifest_path = os.path.join(self._dir, directory, 'MANIFEST') |
| if os.path.exists(manifest_path): |
| os.chmod(manifest_path, 0o644) |
| with open(manifest_path, 'w') as manifest_file: |
| for key, value in manifest.items(): |
| manifest_file.write('%s=%s\n' % (key, value)) |
| |
| def CheckConfig(self): |
| """Checks GomaEnv configurations.""" |
| socket_name = os.environ.get(self._COMPILER_PROXY_IDENTIFIER_ENV_NAME, '') |
| if self._gomacc_socket != socket_name: |
| self._gomacc_socket = socket_name |
| self._gomacc_port = None # invalidate |
| if not os.path.isdir(self._dir): |
| raise ConfigError('%s is not directory' % (self._dir)) |
| if not os.path.isfile(self._compiler_proxy_binary): |
| raise ConfigError('compiler_proxy(%s) not exist' % ( |
| self._compiler_proxy_binary)) |
| if not os.path.isfile(self._gomacc_binary): |
| raise ConfigError('gomacc(%s) not exist' % self._gomacc_binary) |
| self._CheckPlatformConfig() |
| |
| def _GetCompilerProxyPort(self, proc=None): |
| """Gets compiler_proxy's port by "gomacc port". |
| |
| Args: |
| proc: an instance of subprocess.Popen to poll. |
| |
| Returns: |
| a string of compiler proxy port number. |
| |
| Raises: |
| Error: if it cannot get compiler proxy port. |
| """ |
| if self._gomacc_port: |
| return self._gomacc_port |
| |
| port_error = '' |
| stderr = '' |
| |
| ping_start_time = time.time() |
| ping_timeout_sec = int(os.environ.get('GOMA_PING_TIMEOUT_SEC', '0')) + 20 |
| ping_deadline = ping_start_time + ping_timeout_sec |
| ping_print_time = ping_start_time |
| while True: |
| current_time = time.time() |
| if current_time > ping_deadline: |
| break |
| |
| if current_time - ping_print_time > 1: |
| print('waiting for compiler_proxy...') |
| ping_print_time = current_time |
| |
| # output glog output to stderr but ignore it because it is usually about |
| # failure of connecting IPC port. |
| env = os.environ.copy() |
| env['GLOG_logtostderr'] = 'true' |
| with tempfile.TemporaryFile() as tf: |
| # "gomacc port" command may fail until compiler_proxy gets ready. |
| # We know gomacc port only output port number to stdout, whose size |
| # should be within pipe buffer. |
| portcmd = subprocess.Popen([self._gomacc_binary, 'port'], |
| stdout=subprocess.PIPE, |
| stderr=tf, |
| env=env) |
| self._WaitWithTimeout(portcmd, 1) |
| if portcmd.poll() is None: |
| # port takes long time |
| portcmd.kill() |
| port_error = 'port timedout' |
| tf.seek(0) |
| stderr = tf.read() |
| continue |
| portcmd.wait() |
| port = portcmd.stdout.read() |
| tf.seek(0) |
| stderr = tf.read() |
| if port and int(port) != 0: |
| self._gomacc_port = str(int(port)) |
| return self._gomacc_port |
| if proc and not self._is_daemon_mode: |
| proc.poll() |
| if proc.returncode is not None: |
| raise Error('compiler_proxy is not running %d' % proc.returncode) |
| if port_error: |
| print(port_error) |
| if stderr: |
| sys.stderr.write(stderr) |
| e = Error('compiler_proxy is not ready?') |
| self._GetDetailedFailureReason() |
| if proc: |
| e = Error('compiler_proxy is not ready? pid=%d' % proc.pid) |
| if proc.returncode is not None: |
| e = Error('compiler_proxy is not running %d' % proc.returncode) |
| proc.kill() |
| raise e |
| |
| def ControlCompilerProxy(self, command, check_running=True, need_pids=False): |
| """Send comamnd to compiler proxy. |
| |
| Args: |
| command: a string of command to send to the compiler proxy. |
| check_running: True if it needs to check compiler_proxy is running |
| need_pids: True if it needs stakeholder pids. |
| |
| Returns: |
| Dict of boolean status, message string, url prefix, and pids. |
| if need_pids is True, pid field will be stakeholder's pids if |
| compiler_proxy is available. Otherwise, pid='unknown'. |
| """ |
| self.CheckConfig() |
| pids = 'unknown' |
| if check_running and not self.CompilerProxyRunning(): |
| return {'status': False, 'message': 'goma is not running.', 'url': '', |
| 'pid': pids} |
| url_prefix = 'http://127.0.0.1:0' |
| try: |
| url_prefix = 'http://127.0.0.1:%s' % self._GetCompilerProxyPort() |
| url = '%s%s' % (url_prefix, command) |
| # When a user set HTTP proxy environment variables (e.g. http_proxy), |
| # urllib.urlopen uses them even for communicating with compiler_proxy |
| # running in 127.0.0.1, and trying to make the proxy connect to |
| # 127.0.0.1 (i.e. the proxy itself), which should not work. |
| # We should make urllib.urlopen ignore the proxy environment variables. |
| resp = URLOPEN(url, proxies={}) |
| reply = resp.read() |
| if need_pids: |
| pids = ','.join(self._GetStakeholderPids()) |
| return {'status': True, 'message': reply, 'url': url_prefix, 'pid': pids} |
| except (Error, socket.error) as ex: |
| # urllib.urlopen(url) may raise socket.error, such as [Errno 10054] |
| # An existing connection was forcibly closed by the remote host. |
| # socket.error uses IOError as base class in python 2.6. |
| # note: socket.error changed to an alias of OSError in python 3.3. |
| msg = repr(ex) |
| return {'status': False, 'message': msg, 'url': url_prefix, 'pid': pids} |
| |
| def HttpDownload(self, source_url, |
| rewrite_url=None, headers=None, destination_file=None): |
| """Download data from the given URL to the file. |
| |
| If self._goma_fetch defined, prefer goma_fetch to urllib2. |
| |
| Args: |
| source_url: URL to retrieve data. |
| rewrite_url: rewrite source_url for urllib2. |
| headers: a dictionary to be used in the HTTP header. |
| destination_file: file name to store data, if specified None, return |
| contents as string. |
| |
| Returns: |
| None if provided destination_file, downloaded contents otherwise. |
| |
| Raises: |
| Error if fetch failed. |
| """ |
| if self._goma_fetch: |
| # for proxy, goma_fetch uses $GOMA_PROXY_HOST, $GOMA_PROXY_PORT. |
| # headers is used to set Authorization header, but goma_fetch will |
| # set appropriate authorization headers from goma env flags. |
| # increate timeout. |
| env = os.environ |
| env['GOMA_HTTP_SOCKET_READ_TIMEOUT_SECS'] = '300.0' |
| if destination_file: |
| destination_file = os.path.join(self._dir, destination_file) |
| with open(destination_file, 'wb') as f: |
| retcode = subprocess.call([self._goma_fetch, '--auth', source_url], |
| env=env, |
| stdout=f) |
| if retcode: |
| raise Error('failed to fetch %s: %d' % (source_url, retcode)) |
| return |
| return PopenWithCheck([self._goma_fetch, '--auth', source_url], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| env=env).communicate()[0] |
| |
| if rewrite_url: |
| source_url = rewrite_url(source_url) |
| |
| if sys.hexversion < 0x2070900: |
| raise Error('Please use python version >= 2.7.9') |
| |
| http_req = URLREQUEST(source_url) |
| if headers: |
| for name, value in headers.items(): |
| http_req.add_header(name, value) |
| |
| r = URLOPEN2(http_req) |
| if destination_file: |
| with open(os.path.join(self._dir, destination_file), 'wb') as f: |
| shutil.copyfileobj(r, f) |
| return |
| else: |
| return r.read() |
| |
| def GetGomaTmpDir(self): |
| """Get a directory path for goma. |
| |
| Returns: |
| a directory name. |
| """ |
| if self._goma_tmp_dir: |
| return self._goma_tmp_dir |
| |
| if not os.path.exists(self._gomacc_binary): |
| # When installing goma from `goma_ctl.py update`, gomacc does not exist. |
| # In such case, we need to get tempdir from other than gomacc. |
| |
| # TODO: Use tmpfile.TemporaryDirectory in python3 |
| self._goma_tmp_dir = tempfile.mkdtemp() |
| self._delete_tmp_dir = True |
| return self._goma_tmp_dir |
| |
| env = os.environ.copy() |
| env['GLOG_logtostderr'] = 'true' |
| self._goma_tmp_dir = subprocess.check_output( |
| [self._gomacc_binary, 'tmp_dir'], |
| env=env).strip() |
| return self._goma_tmp_dir |
| |
| def GetCrashDumpDirectory(self): |
| """Get a directory path that may contain crash dump. |
| |
| Returns: |
| a directory name. |
| """ |
| return os.path.join(self.GetGomaTmpDir(), _CRASH_DUMP_DIR) |
| |
| def GetCacheDirectory(self): |
| """Get a directory path that may contain cache. |
| |
| Returns: |
| a directory name. |
| """ |
| cache_dir = os.environ.get('GOMA_CACHE_DIR') |
| if cache_dir: |
| return cache_dir |
| |
| return os.path.join(self.GetGomaTmpDir(), _CACHE_DIR) |
| |
| def GetCrashDumps(self): |
| """Get file names of crash dumps. |
| |
| Returns: |
| a list of full qualified crash dump file names. |
| If no crash dump, empty list is returned. |
| """ |
| crash_dir = self.GetCrashDumpDirectory() |
| return glob.glob(os.path.join(crash_dir, '*' + _DUMP_FILE_SUFFIX)) |
| |
| @staticmethod |
| def FindLatestLogFile(command_name, log_type): |
| """Finds latest *.|log_type|.* file. |
| |
| Args: |
| command_name: command name of *.|log_type|.* file. e.g. compiler_proxy. |
| |
| Returns: |
| The latest *.|log_type|.* file path. None if not found. |
| """ |
| |
| info_pattern = os.path.join(_GetLogDirectory(), |
| '%s.*.%s.*' % (command_name, log_type)) |
| candidates = glob.glob(info_pattern) |
| if candidates: |
| return sorted(candidates, reverse=True)[0] |
| return None |
| |
| @classmethod |
| def _GetCompressedLatestLog(cls, command_name, log_type): |
| """Returns compressed log file if exist. |
| |
| Args: |
| command_name: command name of *.|log_type|.* file. e.g. compiler_proxy. |
| |
| Returns: |
| compressed *.|log_type|.* file name. |
| Note: caller should remove the file by themselves. |
| """ |
| logfile = cls.FindLatestLogFile(command_name, log_type) |
| if not logfile: |
| return None |
| |
| tf = tempfile.NamedTemporaryFile(delete=False) |
| with tf as f_out: |
| with gzip.GzipFile(fileobj=f_out) as gzipf_out: |
| with open(logfile) as f_in: |
| shutil.copyfileobj(f_in, gzipf_out) |
| return tf.name |
| |
| @staticmethod |
| def _BuildFormData(form, boundary, out_fh): |
| """Build multipart/form-data from given input. |
| |
| Since we cannot always expect existent of requests module, |
| we need to have the way to build form data by ourselves. |
| Please see also: |
| https://tools.ietf.org/html/rfc2046#section-5.1.1 |
| |
| Args: |
| form: a list of list or tuple that represents form input. |
| first element of each tuple or list would be treated as a name |
| of a form data. |
| If a value starts from '@', it is treated as a file like curl. |
| e.g. [['prod', 'goma'], ['ver', '160']] |
| bondary: a string to represent what value should be used as a boundary. |
| out_fh: file handle to output. Note that you can use StringIO. |
| """ |
| out_fh.write('--%s\r\n' % boundary) |
| if isinstance(form, dict): |
| form = form.items() |
| for i in range(len(form)): |
| name, value = form[i] |
| filename = None |
| content_type = None |
| content = value |
| if isinstance(value, str) and value.startswith('@'): # means file. |
| filename = value[1:] |
| content_type = 'application/octet-stream' |
| with open(filename, 'rb') as f: |
| content = f.read() |
| if filename: |
| out_fh.write('content-disposition: form-data; ' |
| 'name="%s"; ' |
| 'filename="%s"\r\n' % (name, filename)) |
| else: |
| out_fh.write('content-disposition: form-data; name="%s"\r\n' % name) |
| if content_type: |
| out_fh.write('content-type: %s\r\n' % content_type) |
| out_fh.write('\r\n') |
| out_fh.write(content) |
| if i == len(form) - 1: |
| out_fh.write('\r\n--%s--\r\n' % boundary) |
| else: |
| out_fh.write('\r\n--%s\r\n' % boundary) |
| |
| def UploadCrashDump(self, destination_url, product, version, dump_file, |
| guid=None): |
| """Upload crash dump. |
| |
| Args: |
| destination_url: URL to post data. |
| product: a product name string. |
| version: a version number string. |
| dump_file: a dump file name. |
| guid: a unique identifier for this client. |
| |
| Returns: |
| any messages returned by a server. |
| """ |
| form = { |
| 'prod': product, |
| 'ver': version, |
| 'upload_file_minidump': '@%s' % dump_file, |
| } |
| |
| dump_size = -1 |
| try: |
| dump_size = os.path.getsize(dump_file) |
| sys.stderr.write('Crash dump size: %d\n' % dump_size) |
| form['comments'] = 'dump_size:%x' % dump_size |
| except OSError: |
| pass |
| |
| if guid: |
| form['guid'] = guid |
| |
| cp_logfile = None |
| subproc_logfile = None |
| form_body_file = None |
| try: |
| # Since we test goma client with ASan, if we see a goma client crash, |
| # it is usually a crash caused by abort. |
| # In such a case, ERROR logs should be left. |
| # Also, INFO logs are huge, let us avoid to upload them. |
| cp_logfile = self._GetCompressedLatestLog( |
| 'compiler_proxy', 'ERROR') |
| subproc_logfile = self._GetCompressedLatestLog( |
| 'compiler_proxy-subproc', 'ERROR') |
| if cp_logfile: |
| form['compiler_proxy_logfile'] = '@%s' % cp_logfile |
| if subproc_logfile: |
| form['subproc_logfile'] = '@%s' % subproc_logfile |
| |
| boundary = ''.join( |
| random.choice(string.ascii_letters + string.digits) |
| for _ in range(32)) |
| if self._goma_fetch: |
| tf = None |
| try: |
| tf = tempfile.NamedTemporaryFile(delete=False) |
| with tf as f: |
| self._BuildFormData(form, boundary, f) |
| return subprocess.check_output( |
| [self._goma_fetch, |
| '--noauth', |
| '--content_type', |
| 'multipart/form-data; boundary=%s' % boundary, |
| '--data_file', tf.name, |
| '--post', destination_url]) |
| finally: |
| if tf: |
| os.remove(tf.name) |
| |
| if sys.hexversion < 0x2070900: |
| raise Error('Please use python version >= 2.7.9') |
| |
| body = io.StringIO() |
| self._BuildFormData(form, boundary, body) |
| |
| headers = { |
| 'content-type': 'multipart/form-data; boundary=%s' % boundary, |
| } |
| http_req = URLREQUEST(destination_url, data=body.getvalue(), |
| headers=headers) |
| r = URLOPEN2(http_req) |
| out = r.read() |
| finally: |
| if cp_logfile: |
| os.remove(cp_logfile) |
| if subproc_logfile: |
| os.remove(subproc_logfile) |
| if form_body_file: |
| os.remove(form_body_file) |
| return out |
| |
| def WriteFile(self, filename, content): |
| with open(filename, 'wb') as f: |
| f.write(content) |
| |
| def CopyFile(self, from_file, to_file): |
| shutil.copy(from_file, to_file) |
| |
| def MakeTgzFromDirectory(self, dir_name, output_filename): |
| with tarfile.open(output_filename, 'w:gz') as tf: |
| tf.add(dir_name) |
| |
| def RemoveFile(self, filename): |
| filename = os.path.join(self._dir, filename) |
| os.remove(filename) |
| |
| def _ReadBytesFromFile(self, filename, length): |
| filename = os.path.join(self._dir, filename) |
| with open(filename, 'rb') as f: |
| return f.read(length) |
| |
| def IsValidManifest(self, directory=''): |
| contents = self.ReadManifest(directory=directory) |
| |
| if 'PLATFORM' in contents and 'VERSION' in contents: |
| return True |
| return False |
| |
| def IsValidMagic(self, filename): |
| # MANIFEST is special case. |
| if os.path.basename(filename) == 'MANIFEST': |
| return self.IsValidManifest(os.path.dirname(filename)) |
| |
| filename = os.path.join(self._dir, filename) |
| |
| if not os.path.exists(filename): |
| return False |
| |
| magics = { |
| '.tgz': '\x1F\x8B', |
| '.txz': '\xFD7zXZ\x00', |
| '.zip': 'PK', |
| } |
| magic = magics.get(os.path.splitext(filename)[1]) |
| if not magic: |
| return True |
| return self._ReadBytesFromFile(filename, len(magic)) == magic |
| |
| def RemoveDirectory(self, directory): |
| directory = os.path.join(self._dir, directory) |
| shutil.rmtree(directory, ignore_errors=True) |
| |
| def MakeDirectory(self, directory): |
| directory = os.path.join(self._dir, directory) |
| os.mkdir(directory, 0o700) |
| if not os.path.exists(directory): |
| raise Error('Unable to create directory: %s.' % directory) |
| |
| def IsDirectoryExist(self, directory): |
| directory = os.path.join(self._dir, directory) |
| # To avoid symlink attack, the directory should not be symlink. |
| return os.path.isdir(directory) and not os.path.islink(directory) |
| |
| def IsGomaInstalledBefore(self): |
| return os.path.exists(self._compiler_proxy_binary) |
| |
| def IsOldFile(self, filename): |
| log_clean_interval = int(os.environ.get('GOMA_LOG_CLEAN_INTERVAL', '-1')) |
| if log_clean_interval < 0: |
| return False |
| return os.path.getmtime(filename) < self._time - log_clean_interval |
| |
| def _SetupEnviron(self): |
| """Sets default environment variables if they are not configured.""" |
| os.environ['GLOG_logfile_mode'] = str(0o600) |
| for flag_name, default_value in _DEFAULT_ENV: |
| _SetGomaFlagDefaultValueIfEmpty(flag_name, default_value) |
| for flag_name, default_value in self._DEFAULT_ENV: |
| _SetGomaFlagDefaultValueIfEmpty(flag_name, default_value) |
| |
| if not _IsGomaFlagTrue('USE_SSL'): |
| for flag_name, default_value in _DEFAULT_NO_SSL_ENV: |
| _SetGomaFlagDefaultValueIfEmpty(flag_name, default_value) |
| |
| if _IsGomaFlagTrue('USE_SSL'): |
| for flag_name, default_value in self._DEFAULT_SSL_ENV: |
| _SetGomaFlagDefaultValueIfEmpty(flag_name, default_value) |
| |
| def ExecCompilerProxy(self): |
| """Execute compiler proxy in platform dependent way.""" |
| self._gomacc_port = None # invalidate gomacc_port cache. |
| proc = self._ExecCompilerProxy() |
| return self._GetCompilerProxyPort(proc=proc) # set the new gomacc_port. |
| |
| def GetPlatform(self): |
| """Get platform. |
| |
| If the script do not know the platform, it will ask and set platform member |
| varible automatically. |
| |
| Returns: |
| a string of platform. |
| """ |
| if self._platform: |
| return self._platform |
| |
| idx = 1 |
| to_show = [] |
| for candidate in self.PLATFORM_CANDIDATES: |
| to_show.append('%d. %s' % (idx, candidate[0])) |
| idx += 1 |
| print('What is your platform?') |
| print('%s ? --> ' % ' '.join(to_show), end="") |
| selected = sys.stdin.readline() |
| selected = selected.strip() |
| try: |
| self._platform = self.PLATFORM_CANDIDATES[int(selected) - 1][1] |
| except (ValueError, IndexError): |
| raise Error('Invalid selection') |
| return self._platform |
| |
| def CanAutoUpdate(self): |
| """Checks auto update is allowed or not. |
| |
| Returns: |
| True if auto-update is allowed. Otherwise, False. |
| """ |
| if self._version: |
| if not os.path.isfile(os.path.join(self._dir, 'no_auto_update')): |
| return True |
| return False |
| |
| def AutoUpdate(self): |
| """Automatically update the client.""" |
| # Just call myself with update option. |
| script = os.path.join(self._dir, |
| os.path.basename(os.path.realpath(__file__))) |
| subprocess.check_call(['python', script, 'update']) |
| |
| def BackupCurrentPackage(self, backup_dir='backup'): |
| """Back up current pacakge. |
| |
| Args: |
| backup_dir: a string of back up directory name. |
| """ |
| self._backup = [] |
| # ignore parameter in shutil.copytree can be used to remember the copied |
| # files. |
| # See Also: http://docs.python.org/2/library/shutil.html |
| |
| def RememberCopiedFiles(path, names): |
| self._backup.append((path, names)) |
| return [] |
| |
| self.RemoveDirectory(backup_dir) |
| shutil.copytree(self._dir, os.path.join(self._dir, backup_dir), |
| symlinks=True, ignore=RememberCopiedFiles) |
| |
| def RollbackUpdate(self, backup_dir='backup'): |
| """Best-effort-rollback from the backup. |
| |
| Args: |
| backup_dir: a string of back up directory name. |
| |
| Raises: |
| Error: if the caller did not executed BackupCurrentPackage before. Or, |
| rollback failed. |
| """ |
| if not self._backup: |
| raise Error('You should backup files before calling rollback.') |
| for entry in self._backup: |
| backup_dir_path = entry[0].replace(self._dir, |
| os.path.join(self._dir, backup_dir)) |
| # Note: |
| # Somebody may ask "Why not shutil.copytree?" |
| # It is good for back up but not good for rollback. |
| # Since shutil.copytree try to create directories even if it exist, |
| # it will try to make existing directory and cause OSError if we use it |
| # in rollback process. |
| for filename in entry[1]: |
| from_name = os.path.join(backup_dir_path, filename) |
| to_name = os.path.join(entry[0], filename) |
| # After we started to use gomacc to get GomaTmpDir, a file named |
| # UNKNOWN.INFO started to be detected on ChromeOS, which does not exist |
| # on rollback. |
| # Following code is a workaround to avoid failure due to missing |
| # UNKNOWN.INFO. |
| try: |
| from_stat = os.stat(from_name) |
| except OSError as e: |
| sys.stderr.write('cannot access backuped file: %s' % e) |
| continue |
| to_stat = os.stat(to_name) if os.path.exists(to_name) else None |
| # Skip unchanged file / dir. |
| # I expect this also skips running processes since we cannot update it |
| # on Windows. |
| if (to_stat and |
| from_stat.st_size == to_stat.st_size and |
| from_stat.st_mode == to_stat.st_mode and |
| from_stat.st_mtime == to_stat.st_mtime): |
| continue |
| |
| if os.path.isfile(from_name) and os.path.isfile(to_name): |
| shutil.copy2(from_name, to_name) |
| elif os.path.isfile(from_name) and not os.path.exists(to_name): |
| shutil.copy2(from_name, to_name) |
| elif os.path.isdir(from_name) and os.path.isdir(to_name): |
| continue # do nothing if directory exist. |
| elif os.path.isdir(from_name) and not os.path.exists(to_name): |
| self.MakeDirectory(to_name) |
| else: |
| raise Error('Rollback failed. We cannot rollback %s to %s' % |
| (from_name, to_name)) |
| |
| def GetPackageName(self): |
| """Returns package name based on platform.""" |
| return _GetPackageName(self.GetPlatform()) |
| |
| def IsProductionBinary(self): |
| """Returns True if compiler_proxy is release version. |
| |
| Since all of our release binaries are compiled by chrome-bot, |
| we can assume that any binaries compiled by chrome bot would be |
| release or release candidate. |
| |
| Returns: |
| True if compiler_proxy is built by chrome-bot. |
| Otherwise, False. |
| """ |
| info = PopenWithCheck([self._compiler_proxy_binary, '--build-info'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT).communicate()[0].rstrip() |
| return 'built by chrome-bot' in info |
| |
| def _GetExtractedDir(self, update_dir): |
| """Returns a full path directory name where a package is extracted. |
| |
| Args: |
| update_dir: a name of update_dir. This option should be specified when |
| this method is used in update process. |
| |
| Returns: |
| a directory name goma client files are extracted. |
| """ |
| if not update_dir: |
| return self._dir |
| return os.path.join(self._dir, update_dir, |
| 'goma-%s' % self.GetPlatform()) |
| |
| def LoadChecksum(self, update_dir=''): |
| """Returns a dictionary of checksum. |
| |
| For backward compatibility, it returns an empty dictionary if a JSON |
| file does not exist. |
| |
| Args: |
| update_dir: directory containing latest goma files. |
| if empty, load checksum from current goma client directory. |
| |
| Returns: |
| a dictionary of filename and checksum. |
| e.g. {'compiler_proxy': 'abcdef...', ...} |
| """ |
| json_file = os.path.join(self._GetExtractedDir(update_dir), _CHECKSUM_FILE) |
| if not os.path.exists(json_file): |
| print('%s not exist' % json_file) |
| return {} |
| |
| with open(json_file) as f: |
| return json.load(f) |
| |
| def CalculateChecksum(self, filename, update_dir=''): |
| """Returns checksum of a file. |
| |
| Args: |
| filename: a string filename under script dir. |
| update_dir: directory containing latest goma files |
| if empty, calculate checksum of files in current goma client |
| directory. |
| |
| Returns: |
| a checksum of a file. |
| """ |
| return _CalculateChecksum(os.path.join(self._GetExtractedDir(update_dir), |
| filename)) |
| |
| # methods need to be implemented in subclasses. |
| def _ProcessRunning(self, image_name): |
| """Test if any process with image_name is running. |
| |
| Args: |
| image_name: executable image file name |
| |
| Returns: |
| boolean value indicating the result. |
| """ |
| raise NotImplementedError('_ProcessRunning should be implemented.') |
| |
| def _CheckPlatformConfig(self): |
| """Checks platform dependent GomaEnv configurations.""" |
| pass |
| |
| def _ExecCompilerProxy(self): |
| """Execute compiler proxy in platform dependent way.""" |
| raise NotImplementedError('_ExecCompilerProxy should be implemented.') |
| |
| def _GetDetailedFailureReason(self, proc=None): |
| """Gets detailed failure reason if possible.""" |
| pass |
| |
| def ExtractPackage(self, package_file, update_dir): |
| """Extract a platform dependent package. |
| |
| Args: |
| package_file: a filename of package to extract. |
| update_dir: where to extract |
| |
| Returns: |
| boolean indicating success or failure. |
| """ |
| raise NotImplementedError('ExtractPackage should be implemented.') |
| |
| def InstallPackage(self, update_dir): |
| """Overwrite self._dir with files in update_dir. |
| |
| Args: |
| update_dir: directory containing latest goma files |
| |
| Returns: |
| boolean indicating success or failure. |
| """ |
| raise NotImplementedError('InstallPackage should be implemented.') |
| |
| def GetGomaCtlScriptName(self): |
| """Get the name of goma_ctl script to be executed by command line.""" |
| # Subclass may uses its specific variable. |
| # pylint: disable=R0201 |
| return os.environ.get('GOMA_CTL_SCRIPT_NAME', |
| os.path.basename(os.path.realpath(__file__))) |
| |
| @staticmethod |
| def GetPackageExtension(platform): |
| raise NotImplementedError('GetPackageExtension should be implemented.') |
| |
| def CompilerProxyRunning(self): |
| """Returns True if compiler proxy running. |
| |
| Returns: |
| True if compiler_proxy is running. Otherwise, False. |
| """ |
| raise NotImplementedError('CompilerProxyRunning should be implemented.') |
| |
| def KillStakeholders(self, force=False): |
| """Kills stake holder processes. |
| |
| Args: |
| force: kills forcefully. |
| |
| This will kill all processes having locks compiler_proxy needs. |
| """ |
| raise NotImplementedError('KillStakeholders should be implemented.') |
| |
| def WarnNonProtectedFile(self, filename): |
| """Warn if access to the file is not limited. |
| |
| Args: |
| filename: filename to check. |
| """ |
| raise NotImplementedError('WarnNonProtectedFile should be implemented.') |
| |
| def EnsureDirectoryOwnedByUser(self, directory): |
| """Ensure the directory is owned by the user. |
| |
| Args: |
| directory: a name of a directory to be checked. |
| |
| Returns: |
| True if the directory is owned by the user. |
| """ |
| raise NotImplementedError( |
| 'EnsureDirectoryOwnedByUser should be implemented.') |
| |
| def _WaitWithTimeout(self, proc, timeout_sec): |
| """Wait proc finish until timeout_sec. |
| |
| Args: |
| proc: an instance of subprocess.Popen |
| timeout_sec: an integer number to represent timeout in sec. |
| """ |
| raise NotImplementedError |
| |
| def GetUsername(self): |
| user = _GetUsernameEnv() |
| if user: |
| return user |
| user = self._GetUsernameNoEnv() |
| if user: |
| os.environ['USER'] = user |
| return user |
| return 'unknown' |
| |
| def _GetUsernameNoEnv(self): |
| """OS specific way of getting username without environment variables. |
| |
| Returns: |
| a string of an user name if available. If not, empty string. |
| """ |
| raise NotImplementedError |
| |
| |
| class GomaEnvWin(GomaEnv): |
| """Goma running environment for Windows.""" |
| |
| _GOMACC = 'gomacc.exe' |
| _COMPILER_PROXY = 'compiler_proxy.exe' |
| _GOMA_FETCH = 'goma_fetch.exe' |
| # TODO: could be in GomaEnv if env name is the same between |
| # posix and win. |
| _COMPILER_PROXY_IDENTIFIER_ENV_NAME = 'GOMA_COMPILER_PROXY_SOCKET_NAME' |
| _DEFAULT_ENV = [ |
| ('RPC_EXTRA_PARAMS', '?win'), |
| ('COMPILER_PROXY_SOCKET_NAME', 'goma.ipc'), |
| ] |
| _DEFAULT_SSL_ENV = [ |
| # Longer read timeout seems to be required on Windows. |
| ('HTTP_SOCKET_READ_TIMEOUT_SECS', '90.0'), |
| ] |
| PLATFORM_CANDIDATES = [ |
| ('Win64', 'win64'), |
| ] |
| _GOMA_CTL_SCRIPT_NAME = 'goma_ctl.bat' |
| _DEPOT_TOOLS_DIR_PATTERN = re.compile(r'.*[/\\]depot_tools[/\\]?$') |
| |
| def __init__(self): |
| try: |
| self._win32process = __import__('win32process') |
| self._win32api = __import__('win32api') |
| except ImportError as e: |
| print('You don\'t seem to have installed pywin32 module, ' |
| '`pip install pywin32` may fix.') |
| raise e |
| |
| GomaEnv.__init__(self) |
| self._platform = 'win64' |
| |
| @staticmethod |
| def GetPackageExtension(platform): |
| return 'zip' |
| |
| def _ProcessRunning(self, image_name): |
| process = PopenWithCheck(['tasklist', '/FI', |
| 'IMAGENAME eq %s' % image_name], |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| output = process.communicate()[0] |
| return image_name in output |
| |
| def _CheckPlatformConfig(self): |
| """Checks platform dependent GomaEnv configurations.""" |
| if not os.path.isfile(os.path.join(self._dir, 'vcflags.exe')): |
| raise ConfigError('vcflags.exe not found') |
| |
| def _ExecCompilerProxy(self): |
| return PopenWithCheck([self._compiler_proxy_binary], |
| creationflags=self._win32process.DETACHED_PROCESS) |
| |
| def _GetDetailedFailureReason(self, proc=None): |
| pids = self._GetStakeholderPids() |
| print('ports are owned by following processes:') |
| for pid in pids: |
| print(PopenWithCheck(['tasklist', '/FI', 'PID eq %s' % pid], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT).communicate()[0]) |
| |
| def ExtractPackage(self, package_file, update_dir): |
| """Extract a platform dependent package. |
| |
| Args: |
| package_file: a filename of package to extract. |
| update_dir: where to extract |
| |
| Returns: |
| boolean indicating success or failure. |
| |
| Raises: |
| Error: if package does not exist. |
| """ |
| package_file = os.path.join(self._dir, package_file) |
| if not os.path.exists(package_file): |
| raise Error('Expected package file %s does not exist' % package_file) |
| update_dir = os.path.join(self._dir, update_dir) |
| print('Extracting package %s to %s ...' % (package_file, update_dir)) |
| archive = zipfile.ZipFile(package_file) |
| archive.extractall(update_dir) |
| return True |
| |
| def InstallPackage(self, update_dir): |
| """Overwrite self._dir with files in update_dir. |
| |
| Args: |
| update_dir: directory containing latest goma files |
| |
| Returns: |
| boolean indicating success or failure. |
| """ |
| assert update_dir != '' |
| source_dir = self._GetExtractedDir(update_dir) |
| # return code may return non zero even if success. |
| return_code = subprocess.call(['robocopy', source_dir, self._dir, |
| '/ns', '/nc', '/nfl', '/ndl', '/np', |
| '/njh', '/njs']) |
| # ROBOCOPY has very, very interesting error codes. |
| # see http://ss64.com/nt/robocopy-exit.html |
| return return_code < 8 |
| |
| def GetGomaCtlScriptName(self): |
| return os.environ.get('GOMA_CTL_SCRIPT_NAME', self._GOMA_CTL_SCRIPT_NAME) |
| |
| def CompilerProxyRunning(self): |
| return self._ProcessRunning(self._COMPILER_PROXY) |
| |
| def _GetStakeholderPids(self): |
| ports = [] |
| ports.append(os.environ.get('GOMA_COMPILER_PROXY_PORT', '8088')) |
| ns = PopenWithCheck(['netstat', '-a', '-n', '-o'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT).communicate()[0] |
| listenline = re.compile('.*TCP.*(?:%s).*LISTENING *([0-9]*).*' % |
| '|'.join(ports)) |
| pids = set() |
| for line in ns.splitlines(): |
| m = listenline.match(line) |
| if m: |
| pids.add(m.group(1)) |
| return pids |
| |
| def KillStakeholders(self, force=False): |
| pids = self._GetStakeholderPids() |
| if pids: |
| args = [] |
| if force: |
| args.append('/F') |
| for pid in pids: |
| args.extend(['/PID', pid]) |
| subprocess.check_call(['taskkill'] + args) |
| |
| def WarnNonProtectedFile(self, protocol): |
| # TODO: warn for Win. |
| pass |
| |
| def EnsureDirectoryOwnedByUser(self, directory): |
| # TODO: implement for Win. |
| return True |
| |
| def _WaitWithTimeout(self, proc, timeout_sec): |
| import win32api |
| import win32con |
| import win32event |
| try: |
| handle = win32api.OpenProcess( |
| win32con.PROCESS_QUERY_INFORMATION | win32con.SYNCHRONIZE, |
| False, proc.pid) |
| ret = win32event.WaitForSingleObject(handle, timeout_sec * 10**3) |
| if ret in (win32event.WAIT_TIMEOUT, win32event.WAIT_OBJECT_0): |
| return |
| raise Error('WaitForSingleObject returned expected value %s' % ret) |
| finally: |
| if handle: |
| win32api.CloseHandle(handle) |
| |
| def _GetUsernameNoEnv(self): |
| return self._win32api.GetUserName() |
| |
| |
| class GomaEnvPosix(GomaEnv): |
| """Goma running environment for POSIX.""" |
| |
| _GOMACC = 'gomacc' |
| _COMPILER_PROXY = 'compiler_proxy' |
| _GOMA_FETCH = 'goma_fetch' |
| _COMPILER_PROXY_IDENTIFIER_ENV_NAME = 'GOMA_COMPILER_PROXY_SOCKET_NAME' |
| _DEFAULT_ENV = [ |
| # goma_ctl.py runs compiler_proxy in daemon mode by default. |
| ('COMPILER_PROXY_DAEMON_MODE', 'true'), |
| ('COMPILER_PROXY_SOCKET_NAME', 'goma.ipc'), |
| ('COMPILER_PROXY_LOCK_FILENAME', 'goma_compiler_proxy.lock'), |
| ('COMPILER_PROXY_PORT', '8088'), |
| ] |
| PLATFORM_CANDIDATES = [ |
| # (Shown name, platform) |
| ('Goobuntu', 'goobuntu'), |
| ('Chrome OS', 'chromeos'), |
| ('MacOS', 'mac'), |
| ] |
| _LSOF = 'lsof' |
| _FUSER = 'fuser' |
| _FUSER_PID_PATTERN = re.compile(r'(\d+)') |
| _FUSER_USERNAME_PATTERN = re.compile(r'\((\w+)\)') |
| |
| def __init__(self): |
| GomaEnv.__init__(self) |
| # pylint: disable=E1101 |
| # Configure from sysname in uname. |
| if os.uname()[0] == 'Darwin': |
| self._platform = 'mac' |
| self._fuser_path = None |
| self._lsof_path = None |
| self._pwd = __import__('pwd') |
| |
| @staticmethod |
| def GetPackageExtension(platform): |
| return 'tgz' if platform == 'mac' else 'txz' |
| |
| def _ProcessRunning(self, image_name): |
| process = PopenWithCheck(['ps', '-Af'], stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| output = process.communicate()[0] |
| return image_name in output |
| |
| def _ExecCompilerProxy(self): |
| if _IsGomaFlagTrue('COMPILER_PROXY_DAEMON_MODE'): |
| self._is_daemon_mode = True |
| return PopenWithCheck([self._compiler_proxy_binary], |
| stdout=open(os.devnull, "w"), |
| stderr=subprocess.STDOUT) |
| |
| def ExtractPackage(self, package_file, update_dir): |
| """Extract a platform dependent package. |
| |
| Args: |
| package_file: a filename of package to extract. |
| update_dir: where to extract |
| |
| Returns: |
| boolean indicating success or failure. |
| |
| Raises: |
| Error: if package does not exist. |
| """ |
| package_file = os.path.join(self._dir, package_file) |
| if not os.path.exists(package_file): |
| raise Error('Expected package file %s does not exist' % package_file) |
| update_dir = os.path.join(self._dir, update_dir) |
| print('Extracting package to %s ...' % update_dir) |
| if os.path.splitext(package_file)[1] == '.tgz': |
| tar_options = '-zxf' |
| else: |
| tar_options = '-Jxf' |
| return subprocess.call(['tar', tar_options, package_file, '-C', |
| update_dir]) == 0 |
| |
| def InstallPackage(self, update_dir): |
| """Overwrite self._dir with files in update_dir. |
| |
| Args: |
| update_dir: directory containing latest goma files |
| |
| Returns: |
| boolean indicating success or failure. |
| """ |
| assert update_dir != '' |
| # TODO: implement a better version for POSIX |
| source_files = os.path.join(self._GetExtractedDir(update_dir), '*') |
| return subprocess.call(['cp -aRf %s %s' % (source_files, self._dir)], |
| shell=True) == 0 |
| |
| def _ExecLsof(self, cmd): |
| if self._lsof_path is None: |
| self._lsof_path = _FindCommandInPath(self._LSOF) |
| if not self._lsof_path: |
| raise Error('lsof command not found. ' |
| 'Please install lsof command, or put it in PATH.') |
| lsof_command = [self._lsof_path, '-F', 'pun', '-P'] |
| if cmd['type'] == 'process': |
| lsof_command.extend(['-p', cmd['name']]) |
| elif cmd['type'] == 'file': |
| lsof_command.extend([cmd['name']]) |
| elif cmd['type'] == 'network': |
| lsof_command.extend(['-i', cmd['name']]) |
| else: |
| raise Error('unknown cmd type: %s' % cmd) |
| |
| # Lsof returns 1 for WARNING even if the result is good enough. |
| # It also returns 1 if an owner process is not found. |
| ret = subprocess.Popen(lsof_command, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT).communicate()[0] |
| if ret: |
| return _ParseLsof(ret) |
| return [] |
| |
| def _GetOwners(self, name, network=False): |
| """Get owner pid/uid of file or listen port. |
| |
| Args: |
| name: name to check owner. e.g. <tmpdir>/goma.ipc or TCP:8088 |
| network: True if the request is for network socket. |
| |
| Returns: |
| a list of dictionaries containing owner info. |
| """ |
| # os.path.isfile is not feasible to check an unix domain socket. |
| if not network and not os.path.exists(name): |
| return [] |
| |
| if not network and self._GetFuserPath(): |
| (out, err) = subprocess.Popen([self._GetFuserPath(), '-u', name], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE).communicate() |
| if out: # Found at least one owner. |
| pids = self._FUSER_PID_PATTERN.findall(out) |
| usernames = self._FUSER_USERNAME_PATTERN.findall(err) |
| if pids and usernames: |
| uids = [int(self._pwd.getpwnam(x).pw_uid) for x in usernames] |
| return [{'pid': x[0], 'uid': x[1], 'resource': name} |
| for x in zip(pids, uids)] |
| |
| if network: |
| return self._ExecLsof({'type': 'network', 'name': name}) |
| else: |
| return self._ExecLsof({'type': 'file', 'name': name}) |
| |
| def _GetStakeholderPids(self): |
| """Get PID of stake holders. |
| |
| Args: |
| quick: if True, quickly returns result if one of pids is found. |
| |
| Returns: |
| a list of pids holding compiler_proxy locks and a port. |
| |
| Raises: |
| Error: if compiler_proxy's lock is onwed by others. |
| """ |
| # os.getuid does not exist in Windows. |
| # pylint: disable=E1101 |
| tmpdir = self.GetGomaTmpDir() |
| socket_file = os.path.join( |
| tmpdir, os.environ['GOMA_COMPILER_PROXY_SOCKET_NAME']) |
| lock_prefix = os.path.join( |
| tmpdir, os.environ['GOMA_COMPILER_PROXY_LOCK_FILENAME']) |
| port = os.environ['GOMA_COMPILER_PROXY_PORT'] |
| lock_filename = '%s.%s' % (lock_prefix, port) |
| |
| results = [] |
| results.extend(self._GetOwners(socket_file)) |
| results.extend(self._GetOwners(lock_filename)) |
| results.extend(self._GetOwners('TCP:%s' % port, network=True)) |
| |
| uid = os.getuid() |
| if uid != 0: # root can handle any processes. |
| owned_by_others = [x for x in results if x['uid'] != uid] |
| if owned_by_others: |
| raise Error('compiler_proxy lock and/or socket is owned by others.' |
| ' details=%s' % owned_by_others) |
| |
| return set([str(x['pid']) for x in results]) |
| |
| def KillStakeholders(self, force=False): |
| pids = self._GetStakeholderPids() |
| if pids: |
| args = [] |
| if force: |
| args.append('-9') |
| args.extend(list(pids)) |
| subprocess.check_call(['kill'] + args) |
| |
| def _GetFuserPath(self): |
| if self._fuser_path is None: |
| self._fuser_path = _FindCommandInPath(self._FUSER) |
| if not self._fuser_path: |
| self._fuser_path = '' |
| return self._fuser_path |
| |
| def CompilerProxyRunning(self): |
| """Returns True if compiler proxy is running.""" |
| pids = '' |
| try: |
| # note: mac pgrep does not know --delimitor. |
| pids = subprocess.check_output(['pgrep', '-d', ',', |
| self._COMPILER_PROXY]).strip() |
| except subprocess.CalledProcessError as e: |
| if e.returncode == 1: |
| # compiler_proxy is not running. |
| return False |
| else: |
| # should be fatal error. |
| raise e |
| if not pids: |
| raise Error('executed pgrep but result is not given') |
| |
| tmpdir = self.GetGomaTmpDir() |
| socket_file = os.path.join( |
| tmpdir, os.environ['GOMA_COMPILER_PROXY_SOCKET_NAME']) |
| entries = self._ExecLsof({'type': 'process', 'name': pids}) |
| for entry in entries: |
| if socket_file == entry['name']: |
| return True |
| return False |
| |
| def WarnNonProtectedFile(self, filename): |
| # This is platform dependent part. |
| # pylint: disable=R0201 |
| if os.path.exists(filename) and os.stat(filename).st_mode & 0o77: |
| sys.stderr.write( |
| 'We recommend to limit access to the file: %(path)s\n' |
| 'e.g. chmod go-rwx %(path)s\n' % {'path': filename}) |
| |
| def EnsureDirectoryOwnedByUser(self, directory): |
| # This is platform dependent part. |
| # pylint: disable=R0201 |
| # We must use lstat instead of stat to avoid symlink attack (b/69717657). |
| st = os.lstat(directory) |
| if st.st_uid != os.geteuid(): |
| return False |
| try: |
| os.chmod(directory, 0o700) |
| except OSError as err: |
| sys.stderr.write('chmod failure: %s\n' % err) |
| return False |
| return True |
| |
| def _WaitWithTimeout(self, proc, timeout_sec): |
| import signal |
| class TimeoutError(Exception): |
| """Raised on timeout.""" |
| |
| def handle_timeout(_signum, _frame): |
| raise TimeoutError('timed out') |
| |
| signal.signal(signal.SIGALRM, handle_timeout) |
| try: |
| signal.alarm(timeout_sec) |
| proc.wait() |
| except TimeoutError: |
| pass |
| finally: |
| signal.alarm(0) |
| signal.signal(signal.SIGALRM, signal.SIG_DFL) |
| |
| def _GetUsernameNoEnv(self): |
| return self._pwd.getpwuid(os.getuid()).pw_name |
| |
| |
| _GOMA_ENVS = { |
| # os.name, GomaEnv subclass name |
| 'nt': GomaEnvWin, |
| 'posix': GomaEnvPosix, |
| } |
| |
| |
| def _GetPackageName(platform): |
| """Get name of package. |
| |
| Args: |
| platform: a string of platform name. |
| |
| Returns: |
| a string of package name of the given platform. |
| |
| Raises: |
| ConfigError: when given platform is invalid. |
| """ |
| for goma_env in _GOMA_ENVS.values(): |
| supported = [x[1] for x in goma_env.PLATFORM_CANDIDATES] |
| if platform in supported: |
| return 'goma-%s.%s' % (platform, goma_env.GetPackageExtension(platform)) |
| raise ConfigError('Unknown platform %s specified to get package name.' % |
| platform) |
| |
| |
| class GomaBackend(object): |
| """Backend specific configs.""" |
| |
| def __init__(self, env): |
| self._env = env |
| self._download_base_url = None |
| self._path_prefix = None |
| self._stubby_host = None |
| self._SetupEnviron() |
| |
| def _SetupEnviron(self): |
| """Set up backend specific environmental variables.""" |
| pass |
| |
| def _NormalizeBaseUrl(self, resp): |
| """Check the URL is valid, and normalize it if needed. |
| |
| Args: |
| resp: response to the download URL request. |
| |
| Returns: |
| a string of the download base URL. |
| |
| Raises: |
| Error: if the given resp is invalid. |
| """ |
| raise NotImplementedError('Please implement _NormalizeBaseUrl') |
| |
| def GetDownloadBaseUrl(self): |
| """Orchestrate download base url for retrieving manifest file. |
| |
| Returns: |
| The URL string. |
| |
| Raises: |
| Error: if failed to obtain download base URL. |
| """ |
| if self._download_base_url: |
| return self._download_base_url |
| |
| downloadurl_path = '%s/downloadurl' % self._path_prefix |
| downloadurl = 'https://%s%s' % (self._stubby_host, downloadurl_path) |
| url = self._NormalizeBaseUrl( |
| self._env.HttpDownload(downloadurl, |
| rewrite_url=self.RewriteRequest, |
| headers=self.GetHeaders())) |
| if 'GOMACHANNEL' in os.environ: |
| url += '/%s' % os.environ.get('GOMACHANNEL') |
| if url.startswith('http:'): |
| url = 'https:' + url[5:] |
| self._download_base_url = url |
| return url |
| |
| def GetPingUrl(self): |
| """Get ping url. |
| |
| Returns: |
| URL for ping |
| """ |
| return 'https://%s/%s/ping' % (self._stubby_host, self._path_prefix) |
| |
| def RewriteRequest(self, request): |
| """Rewrite request based on backend needs.""" |
| # This usually do not rewrite but subclass may rewrite. |
| # pylint: disable=R0201 |
| return request |
| |
| def GetHeaders(self): |
| """Return headers if there are backend specific headers.""" |
| # This usually returns nothing but subclass may return. |
| # pylint: disable=R0201 |
| return {} |
| |
| |
| class Clients5Backend(GomaBackend): |
| """Backend specific config for Clients5.""" |
| |
| def _SetupEnviron(self): |
| """Set up clients5 backend specific environmental variables.""" |
| # Set member variables for _GetDownloadBaseUrl. |
| self._path_prefix = '/cxx-compiler-service' |
| self._stubby_host = 'clients5.google.com' |
| |
| # TODO: provide better way to make server know Windows client. |
| # Fool proof until we provide the way. |
| if (isinstance(self._env, GomaEnvWin) and |
| not os.environ.get('GOMA_RPC_EXTRA_PARAMS', '')): |
| sys.stderr.write('Please set GOMA_RPC_EXTRA_PARAMS=?win\n') |
| |
| def _NormalizeBaseUrl(self, resp): |
| """Check the URL is valid, and normalize it if needed. |
| |
| Args: |
| resp: response to the download URL request. |
| |
| Returns: |
| a string of the download base URL. |
| |
| Raises: |
| Error: if the given resp is invalid. |
| """ |
| if resp.startswith('http'): |
| return resp |
| msg = 'Could not obtain the download base URL.\n' |
| msg += ('Server response: %s' % resp) |
| raise Error(msg) |
| |
| def GetHeaders(self): |
| """Return headers if there are backend specific headers.""" |
| return {} |
| |
| |
| def GetGomaDriver(): |
| """Returns a proper instance of GomaEnv subclass based on os.name.""" |
| if os.name not in _GOMA_ENVS: |
| raise Error('Could not find proper GomaEnv for "%s"' % os.name) |
| env = _GOMA_ENVS[os.name]() |
| backend = Clients5Backend(env) |
| return GomaDriver(env, backend) |
| |
| |
| def main(): |
| goma = GetGomaDriver() |
| goma.Dispatch(sys.argv[1:]) |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |