blob: e1d1e1171a18607058da2b321ff2894094ec9fe1 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2018 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the', help='License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an', help='AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function
import json
import os
import sys
import time
import urllib
import requests
from absl import app, flags, logging
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
FLAGS = flags.FLAGS
flags.DEFINE_string('environment', 'prod',
'Which symbol server endpoint to use, cemuan be staging or prod')
flags.DEFINE_string(
'api_key', None, 'Which api key to use. By default will load contents of ~/.emulator_symbol_server_key')
flags.DEFINE_string('symbol_file', None, 'The symbol file you wish to process')
flags.DEFINE_boolean(
"force", False, "Upload symbol file, even though it is already available.")
flags.register_validator('environment', lambda value: value.lower() == 'prod' or value.lower() ==
'staging', message='--environment should be either prod or staging (case insensitive).')
flags.mark_flag_as_required('symbol_file')
class SymbolFileServer(object):
"""Class to verify and upload symbols to the crash symbol api v1."""
# The api key should work for both staging and production.
API_KEY_FILE = os.path.join(os.path.expanduser(
'~'), '.emulator_symbol_server_key')
API_URL = {
'prod': 'https://prod-crashsymbolcollector-pa.googleapis.com/v1',
'staging': 'https://staging-crashsymbolcollector-pa.googleapis.com/v1'
}
STATUS_MSG = {
'OK': 'The symbol data was written to symbol storage.',
'DUPLICATE_DATA': 'The symbol data was identical to data already in storage, so no changes were made.'
}
# Old school api, we use this if we have no api key..
CLASSIC_API_URL = {
'staging': 'https://clients2.google.com/cr/staging_symbol',
'prod': 'https://clients2.google.com/cr/symbol'
}
CLASSIC_PRODUCT = 'AndroidEmulator'
# Timeout for individual web requests. an exception is raised if the server has not issued a response for timeout
# seconds (more precisely, if no bytes have been received on the underlying socket for timeout seconds).
DEFAULT_TIMEOUT = 30
# Number of times we will attempt to make the call, including waiting between calls.
MAX_RETRY = 5
def __init__(self, environment='prod', api_key=None):
endpoint = SymbolFileServer.API_URL if api_key else SymbolFileServer.CLASSIC_API_URL
self.api_url = endpoint[environment.lower()]
self.api_key = api_key or ''
def use_classic_api(self):
'''Returns true if we should use the classic api.'''
return not self.api_key
def _extract_symbol_info(self, symbol_file):
'''Extracts os, archictecture, debug id and debug file.
Raises an error if the first line of the file does not contain:
MODULE {os} {arch} {id} {filename}
'''
with open(symbol_file, 'r') as f:
info = f.readline().split()
if len(info) != 5 or info[0] != 'MODULE':
raise IOError('Corrupt symbol file: %s, %s' % (symbol_file, info))
_, os, arch, dbg_id, dbg_file = info
return os, arch, dbg_id, dbg_file
def _exec_request_with_retry(self, oper, url, **kwargs):
'''Makes a web request with default timeout, returning the response.
The request will be tried multiple times, with exponential backoff:
sleep 1 * (2 ^ ({number of total retries} - 1))
'''
retries = Retry(total=SymbolFileServer.MAX_RETRY,
backoff_factor=1, status_forcelist=[502, 503, 504])
with requests.Session() as s:
s.mount('https://', HTTPAdapter(max_retries=retries))
s.mount('http://', HTTPAdapter(max_retries=retries))
return s.request(
oper, url, params={'key': self.api_key}, timeout=SymbolFileServer.DEFAULT_TIMEOUT, **kwargs)
def _exec_request(self, oper, url, **kwargs):
'''Makes a web request with default timeout, returning the json result.
The api key will be added to the url request as a parameter.
This method will raise a requests.exceptions.HTTPError
if the status code is not 4XX, 5XX
Note: If you are using verbose logging it is entirely possible that the subsystem will
write your api key to the logs!
'''
resp = self._exec_request_with_retry(oper, url, **kwargs)
if resp.status_code > 399:
# Make sure we don't leak secret keys by accident.
resp.url = resp.url.replace(
urllib.quote(self.api_key), 'XX-HIDDEN-XX')
logging.error('Url: %s, Status: %s, response: "%s", in: %s',
resp.url, resp.status_code, resp.text, resp.elapsed)
resp.raise_for_status()
if resp.content:
return resp.json()
return {}
def is_available(self, symbol_file):
"""True if the symbol_file is available on the server."""
# The classic api cannot answer this, so assume the symbol_file
# is unavailable.
if self.use_classic_api():
return False
_, _, dbg_id, dbg_file = self._extract_symbol_info(symbol_file)
url = '{}/symbols/{}/{}:checkStatus'.format(
self.api_url, dbg_file, dbg_id)
status = self._exec_request('get', url)
return status['status'] == 'FOUND'
def _upload_with_classic_api(self, symbol_file):
"""Uploads the symbols to old api end point. This is very slow."""
_, _, dbg_id, dbg_file = self._extract_symbol_info(symbol_file)
self._exec_request('post',
self.api_url,
data={
'product': SymbolFileServer.CLASSIC_PRODUCT,
'codeIdentifier': dbg_id,
'debugIdentifier': dbg_id,
'codeFile': dbg_file,
'debugFile': dbg_file,
},
files={
'symbolFile': open(symbol_file, 'rb')
}
)
# We will get exceptions when this fails.
return 'OK'
def upload(self, symbol_file):
"""Makes the symbol_file available on the server, returning the status result."""
if self.use_classic_api():
return self._upload_with_classic_api(symbol_file)
_, _, dbg_id, dbg_file = self._extract_symbol_info(symbol_file)
upload = self._exec_request('post',
'{}/uploads:create'.format(self.api_url))
self._exec_request('put',
upload['uploadUrl'],
data=open(symbol_file, 'r'))
status = self._exec_request('post',
'{}/uploads/{}:complete'.format(
self.api_url, upload['uploadKey']),
data=json.dumps(
{'symbol_id': {'debug_file': dbg_file, 'debug_id': dbg_id}}))
return status['result']
def main(args):
# The lower level enging will log individual requests!
logging.debug(
"-----------------------------------------------------------")
logging.debug(
"- WARNING!! You are likely going to leak your api key --")
logging.debug(
"-----------------------------------------------------------")
api_key = FLAGS.api_key
if not api_key:
try:
with open(SymbolFileServer.API_KEY_FILE) as f:
api_key = f.read()
except IOError as e:
logging.error(
"Unable to read api key due to: %s, reverting to (slow) classic behavior", e)
server = SymbolFileServer(FLAGS.environment, api_key)
if FLAGS.force or not server.is_available(FLAGS.symbol_file):
start = time.time()
status = server.upload(FLAGS.symbol_file)
print("{} after {} seconds".format(SymbolFileServer.STATUS_MSG.get(
status, 'Undefined or unknown result {}'.format(status)), time.time() - start))
else:
print("Symbol already available, skipping upload.")
def launch():
app.run(main)
if __name__ == '__main__':
launch()