blob: de9c152554f8d8e43471b1f807811656a3b6549f [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -04002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Upload all debug symbols required for crash reporting purposes.
7
8This script need only be used to upload release builds symbols or to debug
9crashes on non-release builds (in which case try to only upload the symbols
Mike Frysinger02e1e072013-11-10 22:11:34 -050010for those executables involved).
11"""
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040012
Mike Frysingera4fa1e82014-01-15 01:45:56 -050013from __future__ import print_function
14
Mike Frysingera4fa1e82014-01-15 01:45:56 -050015import httplib
Don Garretta28be6d2016-06-16 18:09:35 -070016import itertools
Mike Nichols90f7c152019-04-09 15:14:08 -060017import json
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040018import os
Mike Frysingerfd355652014-01-23 02:57:48 -050019import socket
Mike Frysingerc5597f22014-11-27 15:39:15 -050020import sys
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040021import textwrap
22import tempfile
23import time
Mike Frysinger094a2172013-08-14 12:54:35 -040024import urllib2
Mike Frysingerd41938e2014-02-10 06:37:55 -050025import urlparse
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040026
Mike Frysinger6db648e2018-07-24 19:57:58 -040027import requests
28
Aviv Keshetb7519e12016-10-04 00:50:00 -070029from chromite.lib import constants
Mike Frysingerbbd1f112016-09-08 18:25:11 -040030
Mike Frysinger3fff4ff2018-07-24 19:24:58 -040031# pylint: disable=ungrouped-imports
Mike Frysingerbbd1f112016-09-08 18:25:11 -040032third_party = os.path.join(constants.CHROMITE_DIR, 'third_party')
33while True:
34 try:
35 sys.path.remove(third_party)
36 except ValueError:
37 break
Mike Frysingerbbd1f112016-09-08 18:25:11 -040038sys.path.insert(0, os.path.join(third_party, 'upload_symbols'))
39del third_party
40
41# Has to be after sys.path manipulation above.
42# And our sys.path muckery confuses pylint.
Mike Frysinger92bdef52019-08-21 21:05:13 -040043# pylint: disable=wrong-import-position
Mike Frysingerbbd1f112016-09-08 18:25:11 -040044import poster # pylint: disable=import-error
45
Mike Frysingerd41938e2014-02-10 06:37:55 -050046from chromite.lib import cache
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040047from chromite.lib import commandline
48from chromite.lib import cros_build_lib
Ralph Nathan5a582ff2015-03-20 18:18:30 -070049from chromite.lib import cros_logging as logging
Mike Frysingerd41938e2014-02-10 06:37:55 -050050from chromite.lib import gs
51from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070052from chromite.lib import path_util
Don Garrette1f47e92016-10-13 16:04:56 -070053from chromite.lib import retry_stats
Mike Frysinger69cb41d2013-08-11 20:08:19 -040054from chromite.scripts import cros_generate_breakpad_symbols
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040055
Mike Frysinger0c0efa22014-02-09 23:32:23 -050056# Needs to be after chromite imports.
Mike Frysingerc5597f22014-11-27 15:39:15 -050057# We don't want to import the general keyring module as that will implicitly
58# try to import & connect to a dbus server. That's a waste of time.
59sys.modules['keyring'] = None
Mike Frysinger0c0efa22014-02-09 23:32:23 -050060
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040061
Don Garretta28be6d2016-06-16 18:09:35 -070062# We need this to run once per process. Do it at module import time as that
63# will let us avoid doing it inline at function call time (see UploadSymbolFile)
64# as that func might be called by the multiprocessing module which means we'll
65# do the opener logic multiple times overall. Plus, if you're importing this
66# module, it's a pretty good chance that you're going to need this.
67poster.streaminghttp.register_openers()
68
69
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040070# URLs used for uploading symbols.
Mike Nichols90f7c152019-04-09 15:14:08 -060071OFFICIAL_UPLOAD_URL = 'https://prod-crashsymbolcollector-pa.googleapis.com/v1'
72STAGING_UPLOAD_URL = 'https://staging-crashsymbolcollector-pa.googleapis.com/v1'
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040073
74# The crash server rejects files that are this big.
Ryo Hashimotof864c0e2017-02-14 12:46:08 +090075CRASH_SERVER_FILE_LIMIT = 700 * 1024 * 1024
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040076# Give ourselves a little breathing room from what the server expects.
77DEFAULT_FILE_LIMIT = CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024)
78
79
Mike Frysinger0c0efa22014-02-09 23:32:23 -050080# The batch limit when talking to the dedup server. We avoid sending one at a
81# time as the round trip overhead will dominate. Conversely, we avoid sending
82# all at once so we can start uploading symbols asap -- the symbol server is a
83# bit slow and will take longer than anything else.
Mike Frysinger0c0efa22014-02-09 23:32:23 -050084DEDUPE_LIMIT = 100
85
86# How long to wait for the server to respond with the results. Note that the
87# larger the limit above, the larger this will need to be. So we give it ~1
88# second per item max.
89DEDUPE_TIMEOUT = DEDUPE_LIMIT
90
Don Garretta28be6d2016-06-16 18:09:35 -070091# How long to wait for the notification to finish (in seconds).
92DEDUPE_NOTIFY_TIMEOUT = 240
Mike Frysinger4dd462e2014-04-30 16:21:51 -040093
Mike Frysinger71046662014-09-12 18:15:15 -070094# The minimum average rate (in bytes per second) that we expect to maintain
95# when uploading symbols. This has to allow for symbols that are up to
96# CRASH_SERVER_FILE_LIMIT in size.
97UPLOAD_MIN_RATE = CRASH_SERVER_FILE_LIMIT / (30 * 60)
98
99# The lowest timeout (in seconds) we'll allow. If the server is overloaded,
100# then there might be a delay in setting up the connection, not just with the
101# transfer. So even a small file might need a larger value.
Ryo Hashimotoc0049372017-02-16 18:50:00 +0900102UPLOAD_MIN_TIMEOUT = 5 * 60
Mike Frysingercd78a082013-06-26 17:13:04 -0400103
104
Don Garrett7a793092016-07-06 16:50:27 -0700105# Sleep for 500ms in between uploads to avoid DoS'ing symbol server.
106SLEEP_DELAY = 0.5
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400107
108
109# Number of seconds to wait before retrying an upload. The delay will double
110# for each subsequent retry of the same symbol file.
111INITIAL_RETRY_DELAY = 1
112
113# Allow up to 7 attempts to upload a symbol file (total delay may be
114# 1+2+4+8+16+32=63 seconds).
115MAX_RETRIES = 6
116
Mike Frysingereb753bf2013-11-22 16:05:35 -0500117# Number of total errors, before uploads are no longer attempted.
118# This is used to avoid lots of errors causing unreasonable delays.
Mike Frysingereb753bf2013-11-22 16:05:35 -0500119MAX_TOTAL_ERRORS_FOR_RETRY = 30
120
Don Garrette1f47e92016-10-13 16:04:56 -0700121# Category to use for collection upload retry stats.
122UPLOAD_STATS = 'UPLOAD'
123
Don Garretta28be6d2016-06-16 18:09:35 -0700124
Don Garretta28be6d2016-06-16 18:09:35 -0700125def BatchGenerator(iterator, batch_size):
126 """Given an iterator, break into lists of size batch_size.
Fang Dengba680462015-08-16 20:34:11 -0700127
Don Garretta28be6d2016-06-16 18:09:35 -0700128 The result is a generator, that will only read in as many inputs as needed for
129 the current batch. The final result can be smaller than batch_size.
Mike Frysinger0a2fd922014-09-12 20:23:42 -0700130 """
Don Garretta28be6d2016-06-16 18:09:35 -0700131 batch = []
132 for i in iterator:
133 batch.append(i)
134 if len(batch) >= batch_size:
135 yield batch
136 batch = []
Mike Frysinger0a2fd922014-09-12 20:23:42 -0700137
Don Garretta28be6d2016-06-16 18:09:35 -0700138 if batch:
139 # if there was anything left in the final batch, yield it.
140 yield batch
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500141
142
Mike Frysingerd41938e2014-02-10 06:37:55 -0500143def IsTarball(path):
144 """Guess if this is a tarball based on the filename."""
145 parts = path.split('.')
146 if len(parts) <= 1:
147 return False
148
149 if parts[-1] == 'tar':
150 return True
151
152 if parts[-2] == 'tar':
153 return parts[-1] in ('bz2', 'gz', 'xz')
154
155 return parts[-1] in ('tbz2', 'tbz', 'tgz', 'txz')
156
157
Don Garretta28be6d2016-06-16 18:09:35 -0700158class SymbolFile(object):
159 """This class represents the state of a symbol file during processing.
160
161 Properties:
162 display_path: Name of symbol file that should be consistent between builds.
163 file_name: Transient path of the symbol file.
164 header: ReadSymsHeader output. Dict with assorted meta-data.
165 status: INITIAL, DUPLICATE, or UPLOADED based on status of processing.
166 dedupe_item: None or instance of DedupeItem for this symbol file.
167 dedupe_push_state: Opaque value to return to dedupe code for file.
168 display_name: Read only friendly (short) file name for logging.
169 file_size: Read only size of the symbol file.
170 """
171 INITIAL = 'initial'
172 DUPLICATE = 'duplicate'
173 UPLOADED = 'uploaded'
Don Garrettdeb2e032016-07-06 16:44:14 -0700174 ERROR = 'error'
Don Garretta28be6d2016-06-16 18:09:35 -0700175
176 def __init__(self, display_path, file_name):
177 """An instance of this class represents a symbol file over time.
178
179 Args:
180 display_path: A unique/persistent between builds name to present to the
181 crash server. It is the file name, relative to where it
182 came from (tarball, breakpad dir, etc).
183 file_name: A the current location of the symbol file.
184 """
185 self.display_path = display_path
186 self.file_name = file_name
187 self.header = cros_generate_breakpad_symbols.ReadSymsHeader(file_name)
188 self.status = SymbolFile.INITIAL
Don Garretta28be6d2016-06-16 18:09:35 -0700189
190 @property
191 def display_name(self):
192 return os.path.basename(self.display_path)
193
194 def FileSize(self):
195 return os.path.getsize(self.file_name)
196
197
Don Garretta28be6d2016-06-16 18:09:35 -0700198def FindSymbolFiles(tempdir, paths):
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500199 """Locate symbol files in |paths|
200
Don Garretta28be6d2016-06-16 18:09:35 -0700201 This returns SymbolFile objects that contain file references which are valid
202 after this exits. Those files may exist externally, or be created in the
203 tempdir (say, when expanding tarballs). The caller must not consider
204 SymbolFile's valid after tempdir is cleaned up.
205
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500206 Args:
Don Garretta28be6d2016-06-16 18:09:35 -0700207 tempdir: Path to use for temporary files.
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500208 paths: A list of input paths to walk. Files are returned w/out any checks.
Mike Frysingerd41938e2014-02-10 06:37:55 -0500209 Dirs are searched for files that end in ".sym". Urls are fetched and then
210 processed. Tarballs are unpacked and walked.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500211
Don Garretta28be6d2016-06-16 18:09:35 -0700212 Yields:
213 A SymbolFile for every symbol file found in paths.
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500214 """
Gilad Arnold83233ed2015-05-08 12:12:13 -0700215 cache_dir = path_util.GetCacheDir()
Mike Frysingere847efd2015-01-08 03:57:24 -0500216 common_path = os.path.join(cache_dir, constants.COMMON_CACHE)
217 tar_cache = cache.TarballCache(common_path)
218
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500219 for p in paths:
Mike Frysingerd41938e2014-02-10 06:37:55 -0500220 o = urlparse.urlparse(p)
Mike Frysinger27e21b72018-07-12 14:20:21 -0400221 if o.scheme:
Mike Frysingerd41938e2014-02-10 06:37:55 -0500222 # Support globs of filenames.
223 ctx = gs.GSContext()
224 for p in ctx.LS(p):
Ralph Nathan03047282015-03-23 11:09:32 -0700225 logging.info('processing files inside %s', p)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500226 o = urlparse.urlparse(p)
Mike Frysinger27e21b72018-07-12 14:20:21 -0400227 key = ('%s%s' % (o.netloc, o.path)).split('/')
Mike Frysingerd41938e2014-02-10 06:37:55 -0500228 # The common cache will not be LRU, removing the need to hold a read
229 # lock on the cached gsutil.
230 ref = tar_cache.Lookup(key)
231 try:
232 ref.SetDefault(p)
233 except cros_build_lib.RunCommandError as e:
Ralph Nathan446aee92015-03-23 14:44:56 -0700234 logging.warning('ignoring %s\n%s', p, e)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500235 continue
Don Garretta28be6d2016-06-16 18:09:35 -0700236 for p in FindSymbolFiles(tempdir, [ref.path]):
Mike Frysingerd41938e2014-02-10 06:37:55 -0500237 yield p
238
239 elif os.path.isdir(p):
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500240 for root, _, files in os.walk(p):
241 for f in files:
242 if f.endswith('.sym'):
Don Garretta28be6d2016-06-16 18:09:35 -0700243 # If p is '/tmp/foo' and filename is '/tmp/foo/bar/bar.sym',
244 # display_path = 'bar/bar.sym'
245 filename = os.path.join(root, f)
246 yield SymbolFile(display_path=filename[len(p):].lstrip('/'),
247 file_name=filename)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500248
249 elif IsTarball(p):
Ralph Nathan03047282015-03-23 11:09:32 -0700250 logging.info('processing files inside %s', p)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500251 tardir = tempfile.mkdtemp(dir=tempdir)
252 cache.Untar(os.path.realpath(p), tardir)
Don Garretta28be6d2016-06-16 18:09:35 -0700253 for p in FindSymbolFiles(tardir, [tardir]):
Mike Frysingerd41938e2014-02-10 06:37:55 -0500254 yield p
255
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500256 else:
Don Garretta28be6d2016-06-16 18:09:35 -0700257 yield SymbolFile(display_path=p, file_name=p)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500258
259
Don Garretta28be6d2016-06-16 18:09:35 -0700260def AdjustSymbolFileSize(symbol, tempdir, file_limit):
261 """Examine symbols files for size problems, and reduce if needed.
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500262
Don Garretta28be6d2016-06-16 18:09:35 -0700263 If the symbols size is too big, strip out the call frame info. The CFI
264 is unnecessary for 32bit x86 targets where the frame pointer is used (as
265 all of ours have) and it accounts for over half the size of the symbols
266 uploaded.
267
268 Stripped files will be created inside tempdir, and will be the callers
269 responsibility to clean up.
270
271 We also warn, if a symbols file is still too large after stripping.
Mike Frysinger5e6dd712014-03-07 22:21:17 -0500272
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500273 Args:
Don Garretta28be6d2016-06-16 18:09:35 -0700274 symbol: SymbolFile instance to be examined and modified as needed..
275 tempdir: A temporary directory we can create files in that the caller will
276 clean up.
277 file_limit: We only strip files which are larger than this limit.
278
279 Returns:
280 SymbolFile instance (original or modified as needed)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500281 """
Don Garretta28be6d2016-06-16 18:09:35 -0700282 file_size = symbol.FileSize()
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500283
Don Garretta28be6d2016-06-16 18:09:35 -0700284 if file_limit and symbol.FileSize() > file_limit:
285 with tempfile.NamedTemporaryFile(
286 prefix='upload_symbols', bufsize=0,
287 dir=tempdir, delete=False) as temp_sym_file:
288
289 temp_sym_file.writelines(
290 [x for x in open(symbol.file_name, 'rb').readlines()
291 if not x.startswith('STACK CFI')]
292 )
293
294 original_file_size = file_size
295 symbol.file_name = temp_sym_file.name
296 file_size = symbol.FileSize()
297
298 logging.warning('stripped CFI for %s reducing size %s > %s',
299 symbol.display_name, original_file_size, file_size)
300
301 # Hopefully the crash server will let it through. But it probably won't.
302 # Not sure what the best answer is in this case.
303 if file_size >= CRASH_SERVER_FILE_LIMIT:
304 logging.PrintBuildbotStepWarnings()
305 logging.warning('upload file %s is awfully large, risking rejection by '
306 'the symbol server (%s > %s)', symbol.display_path,
307 file_size, CRASH_SERVER_FILE_LIMIT)
308
309 return symbol
310
Don Garretta28be6d2016-06-16 18:09:35 -0700311
312def GetUploadTimeout(symbol):
313 """How long to wait for a specific file to upload to the crash server.
314
315 This is a function largely to make unittesting easier.
316
317 Args:
318 symbol: A SymbolFile instance.
319
320 Returns:
321 Timeout length (in seconds)
322 """
323 # Scale the timeout based on the filesize.
324 return max(symbol.FileSize() / UPLOAD_MIN_RATE, UPLOAD_MIN_TIMEOUT)
325
326
Mike Nichols90f7c152019-04-09 15:14:08 -0600327def ExecRequest(operator, url, timeout, api_key, **kwargs):
328 """Makes a web request with default timeout, returning the json result.
Don Garretta28be6d2016-06-16 18:09:35 -0700329
Mike Nichols90f7c152019-04-09 15:14:08 -0600330 This method will raise a requests.exceptions.HTTPError if the status
331 code is not 4XX, 5XX
332
333 Note: If you are using verbose logging it is entirely possible that the
334 subsystem will write your api key to the logs!
335
336 Args:
Mike Nichols649e6a82019-04-23 11:44:48 -0600337 operator: HTTP method.
338 url: Endpoint URL.
339 timeout: HTTP timeout for request.
340 api_key: Authentication key.
Mike Nichols90f7c152019-04-09 15:14:08 -0600341
342 Returns:
343 HTTP response content
344 """
345 resp = requests.request(operator, url,
346 params={'key': api_key},
347 headers={'User-agent': 'chromite.upload_symbols'},
348 timeout=timeout, **kwargs)
349 # Make sure we don't leak secret keys by accident.
350 if resp.status_code > 399:
351 resp.url = resp.url.replace(urllib2.quote(api_key), 'XX-HIDDEN-XX')
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600352 logging.warning('Url: %s, Status: %s, response: "%s", in: %s',
353 resp.url, resp.status_code, resp.text, resp.elapsed)
354 elif resp.content:
Mike Nichols90f7c152019-04-09 15:14:08 -0600355 return resp.json()
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600356
Mike Nichols90f7c152019-04-09 15:14:08 -0600357 return {}
358
359
Mike Nichols137e82d2019-05-15 18:40:34 -0600360def FindDuplicates(symbols, status_url, api_key, timeout=DEDUPE_TIMEOUT):
361 """Check whether the symbol files have already been uploaded.
Mike Nichols649e6a82019-04-23 11:44:48 -0600362
363 Args:
Mike Nichols137e82d2019-05-15 18:40:34 -0600364 symbols: A iterable of SymbolFiles to be uploaded
Mike Nichols649e6a82019-04-23 11:44:48 -0600365 status_url: The crash URL to validate the file existence.
Mike Nichols649e6a82019-04-23 11:44:48 -0600366 api_key: Authentication key.
Mike Nichols137e82d2019-05-15 18:40:34 -0600367 timeout: HTTP timeout for request.
Mike Nichols649e6a82019-04-23 11:44:48 -0600368
369 Yields:
Mike Nichols137e82d2019-05-15 18:40:34 -0600370 All SymbolFiles from symbols, but duplicates have status updated to
371 DUPLICATE.
Mike Nichols649e6a82019-04-23 11:44:48 -0600372 """
Mike Nichols137e82d2019-05-15 18:40:34 -0600373 for batch in BatchGenerator(symbols, DEDUPE_LIMIT):
374 items = []
375 result = {}
376 for x in batch:
377 items.append({'debug_file': x.header.name,
378 'debug_id': x.header.id.replace('-', '')})
379 symbol_data = {'symbol_ids': items}
380 try:
381 result = ExecRequest('post', '%s/symbols:checkStatuses' %
382 status_url,
383 timeout,
384 api_key=api_key,
385 data=json.dumps(symbol_data))
386 except (requests.exceptions.HTTPError,
387 requests.exceptions.Timeout,
388 requests.exceptions.RequestException) as e:
389 logging.warning('could not identify duplicates: HTTP error: %s', e)
390 for b in batch:
391 b.status = SymbolFile.INITIAL
392 set_match = {'debugId': b.header.id.replace('-', ''),
393 'debugFile': b.header.name}
394 for cs_result in result.get('pairs', []):
Mike Frysingerca227ec2019-07-02 17:59:21 -0400395 if set_match == cs_result.get('symbolId'):
Mike Nichols137e82d2019-05-15 18:40:34 -0600396 if cs_result.get('status') == 'FOUND':
397 logging.debug('Found duplicate: %s', b.display_name)
398 b.status = SymbolFile.DUPLICATE
399 break
400 yield b
401
Mike Nichols649e6a82019-04-23 11:44:48 -0600402
Mike Nichols90f7c152019-04-09 15:14:08 -0600403def UploadSymbolFile(upload_url, symbol, api_key):
Mike Frysinger80de5012019-08-01 14:10:53 -0400404 """Upload a symbol file to the crash server, returning the status result.
Don Garretta28be6d2016-06-16 18:09:35 -0700405
406 Args:
407 upload_url: The crash URL to POST the |sym_file| to
408 symbol: A SymbolFile instance.
Mike Nichols90f7c152019-04-09 15:14:08 -0600409 api_key: Authentication key
Mike Frysinger80de5012019-08-01 14:10:53 -0400410 """
Mike Nichols90f7c152019-04-09 15:14:08 -0600411 timeout = GetUploadTimeout(symbol)
Mike Nichols137e82d2019-05-15 18:40:34 -0600412 upload = ExecRequest('post',
413 '%s/uploads:create' % upload_url, timeout, api_key)
Mike Nichols649e6a82019-04-23 11:44:48 -0600414
Mike Nichols137e82d2019-05-15 18:40:34 -0600415 if upload and 'uploadUrl' in upload.keys():
416 symbol_data = {'symbol_id':
417 {'debug_file': symbol.header.name,
418 'debug_id': symbol.header.id.replace('-', '')}
419 }
420 ExecRequest('put',
421 upload['uploadUrl'], timeout,
422 api_key=api_key,
423 data=open(symbol.file_name, 'r'))
424 ExecRequest('post',
425 '%s/uploads/%s:complete' % (
426 upload_url, upload['uploadKey']),
427 timeout, api_key=api_key,
428 # TODO(mikenichols): Validate product_name once it is added
429 # to the proto; currently unsupported.
430 data=json.dumps(symbol_data))
431 else:
432 raise requests.exceptions.HTTPError
Don Garretta28be6d2016-06-16 18:09:35 -0700433
434
Mike Nichols90f7c152019-04-09 15:14:08 -0600435def PerformSymbolsFileUpload(symbols, upload_url, api_key):
Don Garretta28be6d2016-06-16 18:09:35 -0700436 """Upload the symbols to the crash server
437
438 Args:
Don Garrettdeb2e032016-07-06 16:44:14 -0700439 symbols: An iterable of SymbolFiles to be uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700440 upload_url: URL of crash server to upload too.
Mike Nichols90f7c152019-04-09 15:14:08 -0600441 api_key: Authentication key.
Don Garretta28be6d2016-06-16 18:09:35 -0700442 failures: Tracker for total upload failures.
Don Garretta28be6d2016-06-16 18:09:35 -0700443
Don Garrettdeb2e032016-07-06 16:44:14 -0700444 Yields:
445 Each symbol from symbols, perhaps modified.
Don Garretta28be6d2016-06-16 18:09:35 -0700446 """
Don Garrettdeb2e032016-07-06 16:44:14 -0700447 failures = 0
Don Garretta28be6d2016-06-16 18:09:35 -0700448
Don Garrettdeb2e032016-07-06 16:44:14 -0700449 for s in symbols:
450 if (failures < MAX_TOTAL_ERRORS_FOR_RETRY and
451 s.status in (SymbolFile.INITIAL, SymbolFile.ERROR)):
452 # Keeps us from DoS-ing the symbol server.
453 time.sleep(SLEEP_DELAY)
454 logging.info('Uploading symbol_file: %s', s.display_path)
455 try:
456 # This command retries the upload multiple times with growing delays. We
457 # only consider the upload a failure if these retries fail.
Don Garrette1f47e92016-10-13 16:04:56 -0700458 def ShouldRetryUpload(exception):
Mike Nichols0abb8d32019-04-26 14:55:08 -0600459 if isinstance(exception, (requests.exceptions.RequestException,
460 urllib2.URLError,
461 httplib.HTTPException, socket.error)):
462 logging.info('Request failed, retrying: %s', exception)
463 return True
464 return False
Don Garrette1f47e92016-10-13 16:04:56 -0700465
Don Garrett440944e2016-10-03 16:33:31 -0700466 with cros_build_lib.TimedSection() as timer:
Don Garrette1f47e92016-10-13 16:04:56 -0700467 retry_stats.RetryWithStats(
468 UPLOAD_STATS, ShouldRetryUpload, MAX_RETRIES,
Don Garrett440944e2016-10-03 16:33:31 -0700469 UploadSymbolFile,
Mike Nichols90f7c152019-04-09 15:14:08 -0600470 upload_url, s, api_key,
Luigi Semenzato5104f3c2016-10-12 12:37:42 -0700471 sleep=INITIAL_RETRY_DELAY,
472 log_all_retries=True)
Mike Nicholsb03b2c62019-05-02 18:46:04 -0600473 if s.status != SymbolFile.DUPLICATE:
474 logging.info('upload of %10i bytes took %s', s.FileSize(),
475 timer.delta)
476 s.status = SymbolFile.UPLOADED
Mike Nichols90f7c152019-04-09 15:14:08 -0600477 except (requests.exceptions.HTTPError,
478 requests.exceptions.Timeout,
479 requests.exceptions.RequestException) as e:
480 logging.warning('could not upload: %s: HTTP error: %s',
481 s.display_name, e)
Don Garrettdeb2e032016-07-06 16:44:14 -0700482 s.status = SymbolFile.ERROR
483 failures += 1
Mike Nichols90f7c152019-04-09 15:14:08 -0600484 except (httplib.HTTPException, urllib2.URLError, socket.error) as e:
Ryo Hashimoto45273f02017-03-16 17:45:56 +0900485 logging.warning('could not upload: %s: %s %s', s.display_name,
486 type(e).__name__, e)
Don Garrettdeb2e032016-07-06 16:44:14 -0700487 s.status = SymbolFile.ERROR
488 failures += 1
489
490 # We pass the symbol along, on both success and failure.
491 yield s
Don Garretta28be6d2016-06-16 18:09:35 -0700492
493
Don Garrettdeb2e032016-07-06 16:44:14 -0700494def ReportResults(symbols, failed_list):
Don Garretta28be6d2016-06-16 18:09:35 -0700495 """Log a summary of the symbol uploading.
496
497 This has the side effect of fully consuming the symbols iterator.
498
499 Args:
500 symbols: An iterator of SymbolFiles to be uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700501 failed_list: A filename at which to write out a list of our failed uploads.
Don Garrettdeb2e032016-07-06 16:44:14 -0700502
503 Returns:
504 The number of symbols not uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700505 """
506 upload_failures = []
507 result_counts = {
508 SymbolFile.INITIAL: 0,
509 SymbolFile.UPLOADED: 0,
510 SymbolFile.DUPLICATE: 0,
Don Garrettdeb2e032016-07-06 16:44:14 -0700511 SymbolFile.ERROR: 0,
Don Garretta28be6d2016-06-16 18:09:35 -0700512 }
513
514 for s in symbols:
515 result_counts[s.status] += 1
Don Garrettdeb2e032016-07-06 16:44:14 -0700516 if s.status in [SymbolFile.INITIAL, SymbolFile.ERROR]:
Don Garretta28be6d2016-06-16 18:09:35 -0700517 upload_failures.append(s)
518
Don Garrette1f47e92016-10-13 16:04:56 -0700519 # Report retry numbers.
520 _, _, retries = retry_stats.CategoryStats(UPLOAD_STATS)
521 if retries:
522 logging.warning('%d upload retries performed.', retries)
523
Don Garretta28be6d2016-06-16 18:09:35 -0700524 logging.info('Uploaded %(uploaded)d, Skipped %(duplicate)d duplicates.',
525 result_counts)
526
Chris Ching91908032016-09-27 16:55:33 -0600527 if result_counts[SymbolFile.ERROR]:
Don Garretta28be6d2016-06-16 18:09:35 -0700528 logging.PrintBuildbotStepWarnings()
Chris Ching91908032016-09-27 16:55:33 -0600529 logging.warning('%d non-recoverable upload errors',
530 result_counts[SymbolFile.ERROR])
531
532 if result_counts[SymbolFile.INITIAL]:
533 logging.PrintBuildbotStepWarnings()
534 logging.warning('%d upload(s) were skipped because of excessive errors',
Don Garrettdeb2e032016-07-06 16:44:14 -0700535 result_counts[SymbolFile.INITIAL])
Don Garretta28be6d2016-06-16 18:09:35 -0700536
537 if failed_list is not None:
538 with open(failed_list, 'w') as fl:
539 for s in upload_failures:
540 fl.write('%s\n' % s.display_path)
541
Don Garrettdeb2e032016-07-06 16:44:14 -0700542 return result_counts[SymbolFile.INITIAL] + result_counts[SymbolFile.ERROR]
543
Don Garretta28be6d2016-06-16 18:09:35 -0700544
Mike Nichols649e6a82019-04-23 11:44:48 -0600545def UploadSymbols(sym_paths, upload_url, failed_list=None,
Mike Nichols137e82d2019-05-15 18:40:34 -0600546 upload_limit=None, strip_cfi=None, timeout=None,
Mike Nichols90f7c152019-04-09 15:14:08 -0600547 api_key=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400548 """Upload all the generated symbols for |board| to the crash server
549
550 Args:
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500551 sym_paths: Specific symbol files (or dirs of sym files) to upload,
552 otherwise search |breakpad_dir|
Don Garretta28be6d2016-06-16 18:09:35 -0700553 upload_url: URL of crash server to upload too.
Don Garretta28be6d2016-06-16 18:09:35 -0700554 failed_list: A filename at which to write out a list of our failed uploads.
555 upload_limit: Integer listing how many files to upload. None for no limit.
556 strip_cfi: File size at which we strip out CFI data. None for no limit.
Mike Nichols137e82d2019-05-15 18:40:34 -0600557 timeout: HTTP timeout for request.
Mike Nichols90f7c152019-04-09 15:14:08 -0600558 api_key: A string based authentication key
Mike Frysinger1a736a82013-12-12 01:50:59 -0500559
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400560 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400561 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400562 """
Don Garrette1f47e92016-10-13 16:04:56 -0700563 retry_stats.SetupStats()
564
Don Garretta28be6d2016-06-16 18:09:35 -0700565 # Note: This method looks like each step of processing is performed
566 # sequentially for all SymbolFiles, but instead each step is a generator that
567 # produces the next iteration only when it's read. This means that (except for
568 # some batching) each SymbolFile goes through all of these steps before the
569 # next one is processed at all.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400570
Don Garretta28be6d2016-06-16 18:09:35 -0700571 # This is used to hold striped
572 with osutils.TempDir(prefix='upload_symbols.') as tempdir:
573 symbols = FindSymbolFiles(tempdir, sym_paths)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500574
Don Garrett1bc1e102016-07-06 17:06:10 -0700575 # Sort all of our symbols so the largest ones (probably the most important)
576 # are processed first.
577 symbols = list(symbols)
578 symbols.sort(key=lambda s: s.FileSize(), reverse=True)
579
Don Garretta28be6d2016-06-16 18:09:35 -0700580 if upload_limit is not None:
581 # Restrict symbols processed to the limit.
582 symbols = itertools.islice(symbols, None, upload_limit)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400583
Don Garretta28be6d2016-06-16 18:09:35 -0700584 # Strip CFI, if needed.
585 symbols = (AdjustSymbolFileSize(s, tempdir, strip_cfi) for s in symbols)
Mike Frysinger8ec8c502014-02-10 00:19:13 -0500586
Mike Nichols137e82d2019-05-15 18:40:34 -0600587 # Find duplicates via batch API
588 symbols = FindDuplicates(symbols, upload_url, api_key, timeout)
589
Don Garretta28be6d2016-06-16 18:09:35 -0700590 # Perform uploads
Mike Nichols90f7c152019-04-09 15:14:08 -0600591 symbols = PerformSymbolsFileUpload(symbols, upload_url, api_key)
Mike Frysingerd42e5f02014-03-14 11:19:37 -0400592
Don Garretta28be6d2016-06-16 18:09:35 -0700593 # Log the final results, and consume the symbols generator fully.
Don Garrettdeb2e032016-07-06 16:44:14 -0700594 failures = ReportResults(symbols, failed_list)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500595
Don Garrettdeb2e032016-07-06 16:44:14 -0700596 return failures
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400597
598
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400599def main(argv):
600 parser = commandline.ArgumentParser(description=__doc__)
601
Don Garretta28be6d2016-06-16 18:09:35 -0700602 # TODO: Make sym_paths, breakpad_root, and root exclusive.
603
Mike Frysingerd41938e2014-02-10 06:37:55 -0500604 parser.add_argument('sym_paths', type='path_or_uri', nargs='*', default=None,
605 help='symbol file or directory or URL or tarball')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400606 parser.add_argument('--board', default=None,
Don Garrett747cc4b2015-10-07 14:48:48 -0700607 help='Used to find default breakpad_root.')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400608 parser.add_argument('--breakpad_root', type='path', default=None,
Mike Frysingerc5597f22014-11-27 15:39:15 -0500609 help='full path to the breakpad symbol directory')
610 parser.add_argument('--root', type='path', default=None,
611 help='full path to the chroot dir')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400612 parser.add_argument('--official_build', action='store_true', default=False,
613 help='point to official symbol server')
Mike Frysinger38647542014-09-12 18:15:39 -0700614 parser.add_argument('--server', type=str, default=None,
615 help='URI for custom symbol server')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400616 parser.add_argument('--regenerate', action='store_true', default=False,
617 help='regenerate all symbols')
Don Garretta28be6d2016-06-16 18:09:35 -0700618 parser.add_argument('--upload-limit', type=int,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400619 help='only upload # number of symbols')
620 parser.add_argument('--strip_cfi', type=int,
Don Garretta28be6d2016-06-16 18:09:35 -0700621 default=DEFAULT_FILE_LIMIT,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400622 help='strip CFI data for files above this size')
Mike Frysinger7f9be142014-01-15 02:16:42 -0500623 parser.add_argument('--failed-list', type='path',
624 help='where to save a list of failed symbols')
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500625 parser.add_argument('--dedupe', action='store_true', default=False,
626 help='use the swarming service to avoid re-uploading')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400627 parser.add_argument('--yes', action='store_true', default=False,
628 help='answer yes to all prompts')
Don Garrett747cc4b2015-10-07 14:48:48 -0700629 parser.add_argument('--product_name', type=str, default='ChromeOS',
630 help='Produce Name for breakpad stats.')
Mike Nichols90f7c152019-04-09 15:14:08 -0600631 parser.add_argument('--api_key', type=str, default=None,
632 help='full path to the API key file')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400633
634 opts = parser.parse_args(argv)
Mike Frysinger90e49ca2014-01-14 14:42:07 -0500635 opts.Freeze()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400636
Don Garretta28be6d2016-06-16 18:09:35 -0700637 # Figure out the symbol files/directories to upload.
638 if opts.sym_paths:
639 sym_paths = opts.sym_paths
640 elif opts.breakpad_root:
641 sym_paths = [opts.breakpad_root]
642 elif opts.root:
643 if not opts.board:
Mike Frysinger8390dec2017-08-11 15:11:38 -0400644 cros_build_lib.Die('--board must be set if --root is used.')
Don Garretta28be6d2016-06-16 18:09:35 -0700645 breakpad_dir = cros_generate_breakpad_symbols.FindBreakpadDir(opts.board)
646 sym_paths = [os.path.join(opts.root, breakpad_dir.lstrip('/'))]
647 else:
Mike Frysinger8390dec2017-08-11 15:11:38 -0400648 cros_build_lib.Die('--sym_paths, --breakpad_root, or --root must be set.')
Don Garretta28be6d2016-06-16 18:09:35 -0700649
Don Garrett747cc4b2015-10-07 14:48:48 -0700650 if opts.sym_paths or opts.breakpad_root:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400651 if opts.regenerate:
Don Garrett747cc4b2015-10-07 14:48:48 -0700652 cros_build_lib.Die('--regenerate may not be used with specific files, '
653 'or breakpad_root')
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400654 else:
655 if opts.board is None:
656 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400657
Don Garretta28be6d2016-06-16 18:09:35 -0700658 # Figure out which crash server to upload too.
659 upload_url = opts.server
660 if not upload_url:
661 if opts.official_build:
662 upload_url = OFFICIAL_UPLOAD_URL
663 else:
664 logging.warning('unofficial builds upload to the staging server')
665 upload_url = STAGING_UPLOAD_URL
666
Mike Nichols90f7c152019-04-09 15:14:08 -0600667 # Set up the API key needed to authenticate to Crash server.
668 # Allow for a local key file for testing purposes.
669 if opts.api_key:
670 api_key_file = opts.api_key
671 else:
672 api_key_file = constants.CRASH_API_KEY
673
674 api_key = osutils.ReadFile(api_key_file)
675
Don Garretta28be6d2016-06-16 18:09:35 -0700676 # Confirm we really want the long upload.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400677 if not opts.yes:
Mike Frysingerc5de9602014-02-09 02:42:36 -0500678 prolog = '\n'.join(textwrap.wrap(textwrap.dedent("""
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400679 Uploading symbols for an entire Chromium OS build is really only
680 necessary for release builds and in a few cases for developers
681 to debug problems. It will take considerable time to run. For
682 developer debugging purposes, consider instead passing specific
683 files to upload.
Mike Frysingerc5de9602014-02-09 02:42:36 -0500684 """), 80)).strip()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400685 if not cros_build_lib.BooleanPrompt(
686 prompt='Are you sure you want to upload all build symbols',
Mike Frysingerc5de9602014-02-09 02:42:36 -0500687 default=False, prolog=prolog):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400688 cros_build_lib.Die('better safe than sorry')
689
690 ret = 0
Don Garretta28be6d2016-06-16 18:09:35 -0700691
692 # Regenerate symbols from binaries.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400693 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400694 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
695 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400696
Don Garretta28be6d2016-06-16 18:09:35 -0700697 # Do the upload.
698 ret += UploadSymbols(
699 sym_paths=sym_paths,
700 upload_url=upload_url,
Don Garretta28be6d2016-06-16 18:09:35 -0700701 failed_list=opts.failed_list,
702 upload_limit=opts.upload_limit,
Mike Nichols90f7c152019-04-09 15:14:08 -0600703 strip_cfi=opts.strip_cfi,
704 api_key=api_key)
Don Garretta28be6d2016-06-16 18:09:35 -0700705
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400706 if ret:
Ralph Nathan59900422015-03-24 10:41:17 -0700707 logging.error('encountered %i problem(s)', ret)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400708 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
709 # return 0 in case we are a multiple of the mask.
Don Garretta28be6d2016-06-16 18:09:35 -0700710 return 1