blob: 832b9e1d250790e7d7417c32cb8fcd48bc1b3f83 [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
24
Mike Frysinger6db648e2018-07-24 19:57:58 -040025import requests
Mike Frysinger3dcacee2019-08-23 17:09:11 -040026from six.moves import urllib
Mike Frysinger6db648e2018-07-24 19:57:58 -040027
Aviv Keshetb7519e12016-10-04 00:50:00 -070028from chromite.lib import constants
Mike Frysingerbbd1f112016-09-08 18:25:11 -040029
Mike Frysinger3fff4ff2018-07-24 19:24:58 -040030# pylint: disable=ungrouped-imports
Mike Frysingerbbd1f112016-09-08 18:25:11 -040031third_party = os.path.join(constants.CHROMITE_DIR, 'third_party')
32while True:
33 try:
34 sys.path.remove(third_party)
35 except ValueError:
36 break
Mike Frysingerbbd1f112016-09-08 18:25:11 -040037sys.path.insert(0, os.path.join(third_party, 'upload_symbols'))
38del third_party
39
40# Has to be after sys.path manipulation above.
41# And our sys.path muckery confuses pylint.
Mike Frysinger92bdef52019-08-21 21:05:13 -040042# pylint: disable=wrong-import-position
Mike Frysingerbbd1f112016-09-08 18:25:11 -040043import poster # pylint: disable=import-error
44
Mike Frysingerd41938e2014-02-10 06:37:55 -050045from chromite.lib import cache
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040046from chromite.lib import commandline
47from chromite.lib import cros_build_lib
Ralph Nathan5a582ff2015-03-20 18:18:30 -070048from chromite.lib import cros_logging as logging
Mike Frysingerd41938e2014-02-10 06:37:55 -050049from chromite.lib import gs
50from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070051from chromite.lib import path_util
Don Garrette1f47e92016-10-13 16:04:56 -070052from chromite.lib import retry_stats
Mike Frysinger69cb41d2013-08-11 20:08:19 -040053from chromite.scripts import cros_generate_breakpad_symbols
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040054
Mike Frysinger0c0efa22014-02-09 23:32:23 -050055# Needs to be after chromite imports.
Mike Frysingerc5597f22014-11-27 15:39:15 -050056# We don't want to import the general keyring module as that will implicitly
57# try to import & connect to a dbus server. That's a waste of time.
58sys.modules['keyring'] = None
Mike Frysinger0c0efa22014-02-09 23:32:23 -050059
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040060
Don Garretta28be6d2016-06-16 18:09:35 -070061# We need this to run once per process. Do it at module import time as that
62# will let us avoid doing it inline at function call time (see UploadSymbolFile)
63# as that func might be called by the multiprocessing module which means we'll
64# do the opener logic multiple times overall. Plus, if you're importing this
65# module, it's a pretty good chance that you're going to need this.
66poster.streaminghttp.register_openers()
67
68
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040069# URLs used for uploading symbols.
Mike Nichols90f7c152019-04-09 15:14:08 -060070OFFICIAL_UPLOAD_URL = 'https://prod-crashsymbolcollector-pa.googleapis.com/v1'
71STAGING_UPLOAD_URL = 'https://staging-crashsymbolcollector-pa.googleapis.com/v1'
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040072
73# The crash server rejects files that are this big.
Ryo Hashimotof864c0e2017-02-14 12:46:08 +090074CRASH_SERVER_FILE_LIMIT = 700 * 1024 * 1024
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040075# Give ourselves a little breathing room from what the server expects.
76DEFAULT_FILE_LIMIT = CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024)
77
78
Mike Frysinger0c0efa22014-02-09 23:32:23 -050079# The batch limit when talking to the dedup server. We avoid sending one at a
80# time as the round trip overhead will dominate. Conversely, we avoid sending
81# all at once so we can start uploading symbols asap -- the symbol server is a
82# bit slow and will take longer than anything else.
Mike Frysinger0c0efa22014-02-09 23:32:23 -050083DEDUPE_LIMIT = 100
84
85# How long to wait for the server to respond with the results. Note that the
86# larger the limit above, the larger this will need to be. So we give it ~1
87# second per item max.
88DEDUPE_TIMEOUT = DEDUPE_LIMIT
89
Don Garretta28be6d2016-06-16 18:09:35 -070090# How long to wait for the notification to finish (in seconds).
91DEDUPE_NOTIFY_TIMEOUT = 240
Mike Frysinger4dd462e2014-04-30 16:21:51 -040092
Mike Frysinger71046662014-09-12 18:15:15 -070093# The minimum average rate (in bytes per second) that we expect to maintain
94# when uploading symbols. This has to allow for symbols that are up to
95# CRASH_SERVER_FILE_LIMIT in size.
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040096UPLOAD_MIN_RATE = CRASH_SERVER_FILE_LIMIT // (30 * 60)
Mike Frysinger71046662014-09-12 18:15:15 -070097
98# The lowest timeout (in seconds) we'll allow. If the server is overloaded,
99# then there might be a delay in setting up the connection, not just with the
100# transfer. So even a small file might need a larger value.
Ryo Hashimotoc0049372017-02-16 18:50:00 +0900101UPLOAD_MIN_TIMEOUT = 5 * 60
Mike Frysingercd78a082013-06-26 17:13:04 -0400102
103
Don Garrett7a793092016-07-06 16:50:27 -0700104# Sleep for 500ms in between uploads to avoid DoS'ing symbol server.
105SLEEP_DELAY = 0.5
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400106
107
108# Number of seconds to wait before retrying an upload. The delay will double
109# for each subsequent retry of the same symbol file.
110INITIAL_RETRY_DELAY = 1
111
112# Allow up to 7 attempts to upload a symbol file (total delay may be
113# 1+2+4+8+16+32=63 seconds).
114MAX_RETRIES = 6
115
Mike Frysingereb753bf2013-11-22 16:05:35 -0500116# Number of total errors, before uploads are no longer attempted.
117# This is used to avoid lots of errors causing unreasonable delays.
Mike Frysingereb753bf2013-11-22 16:05:35 -0500118MAX_TOTAL_ERRORS_FOR_RETRY = 30
119
Don Garrette1f47e92016-10-13 16:04:56 -0700120# Category to use for collection upload retry stats.
121UPLOAD_STATS = 'UPLOAD'
122
Don Garretta28be6d2016-06-16 18:09:35 -0700123
Don Garretta28be6d2016-06-16 18:09:35 -0700124def BatchGenerator(iterator, batch_size):
125 """Given an iterator, break into lists of size batch_size.
Fang Dengba680462015-08-16 20:34:11 -0700126
Don Garretta28be6d2016-06-16 18:09:35 -0700127 The result is a generator, that will only read in as many inputs as needed for
128 the current batch. The final result can be smaller than batch_size.
Mike Frysinger0a2fd922014-09-12 20:23:42 -0700129 """
Don Garretta28be6d2016-06-16 18:09:35 -0700130 batch = []
131 for i in iterator:
132 batch.append(i)
133 if len(batch) >= batch_size:
134 yield batch
135 batch = []
Mike Frysinger0a2fd922014-09-12 20:23:42 -0700136
Don Garretta28be6d2016-06-16 18:09:35 -0700137 if batch:
138 # if there was anything left in the final batch, yield it.
139 yield batch
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500140
141
Mike Frysingerd41938e2014-02-10 06:37:55 -0500142def IsTarball(path):
143 """Guess if this is a tarball based on the filename."""
144 parts = path.split('.')
145 if len(parts) <= 1:
146 return False
147
148 if parts[-1] == 'tar':
149 return True
150
151 if parts[-2] == 'tar':
152 return parts[-1] in ('bz2', 'gz', 'xz')
153
154 return parts[-1] in ('tbz2', 'tbz', 'tgz', 'txz')
155
156
Don Garretta28be6d2016-06-16 18:09:35 -0700157class SymbolFile(object):
158 """This class represents the state of a symbol file during processing.
159
160 Properties:
161 display_path: Name of symbol file that should be consistent between builds.
162 file_name: Transient path of the symbol file.
163 header: ReadSymsHeader output. Dict with assorted meta-data.
164 status: INITIAL, DUPLICATE, or UPLOADED based on status of processing.
165 dedupe_item: None or instance of DedupeItem for this symbol file.
166 dedupe_push_state: Opaque value to return to dedupe code for file.
167 display_name: Read only friendly (short) file name for logging.
168 file_size: Read only size of the symbol file.
169 """
170 INITIAL = 'initial'
171 DUPLICATE = 'duplicate'
172 UPLOADED = 'uploaded'
Don Garrettdeb2e032016-07-06 16:44:14 -0700173 ERROR = 'error'
Don Garretta28be6d2016-06-16 18:09:35 -0700174
175 def __init__(self, display_path, file_name):
176 """An instance of this class represents a symbol file over time.
177
178 Args:
179 display_path: A unique/persistent between builds name to present to the
180 crash server. It is the file name, relative to where it
181 came from (tarball, breakpad dir, etc).
182 file_name: A the current location of the symbol file.
183 """
184 self.display_path = display_path
185 self.file_name = file_name
186 self.header = cros_generate_breakpad_symbols.ReadSymsHeader(file_name)
187 self.status = SymbolFile.INITIAL
Don Garretta28be6d2016-06-16 18:09:35 -0700188
189 @property
190 def display_name(self):
191 return os.path.basename(self.display_path)
192
193 def FileSize(self):
194 return os.path.getsize(self.file_name)
195
196
Don Garretta28be6d2016-06-16 18:09:35 -0700197def FindSymbolFiles(tempdir, paths):
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500198 """Locate symbol files in |paths|
199
Don Garretta28be6d2016-06-16 18:09:35 -0700200 This returns SymbolFile objects that contain file references which are valid
201 after this exits. Those files may exist externally, or be created in the
202 tempdir (say, when expanding tarballs). The caller must not consider
203 SymbolFile's valid after tempdir is cleaned up.
204
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500205 Args:
Don Garretta28be6d2016-06-16 18:09:35 -0700206 tempdir: Path to use for temporary files.
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500207 paths: A list of input paths to walk. Files are returned w/out any checks.
Mike Frysingerd41938e2014-02-10 06:37:55 -0500208 Dirs are searched for files that end in ".sym". Urls are fetched and then
209 processed. Tarballs are unpacked and walked.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500210
Don Garretta28be6d2016-06-16 18:09:35 -0700211 Yields:
212 A SymbolFile for every symbol file found in paths.
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500213 """
Gilad Arnold83233ed2015-05-08 12:12:13 -0700214 cache_dir = path_util.GetCacheDir()
Mike Frysingere847efd2015-01-08 03:57:24 -0500215 common_path = os.path.join(cache_dir, constants.COMMON_CACHE)
216 tar_cache = cache.TarballCache(common_path)
217
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500218 for p in paths:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400219 o = urllib.parse.urlparse(p)
Mike Frysinger27e21b72018-07-12 14:20:21 -0400220 if o.scheme:
Mike Frysingerd41938e2014-02-10 06:37:55 -0500221 # Support globs of filenames.
222 ctx = gs.GSContext()
223 for p in ctx.LS(p):
Ralph Nathan03047282015-03-23 11:09:32 -0700224 logging.info('processing files inside %s', p)
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400225 o = urllib.parse.urlparse(p)
Mike Frysinger27e21b72018-07-12 14:20:21 -0400226 key = ('%s%s' % (o.netloc, o.path)).split('/')
Mike Frysingerd41938e2014-02-10 06:37:55 -0500227 # The common cache will not be LRU, removing the need to hold a read
228 # lock on the cached gsutil.
229 ref = tar_cache.Lookup(key)
230 try:
231 ref.SetDefault(p)
232 except cros_build_lib.RunCommandError as e:
Ralph Nathan446aee92015-03-23 14:44:56 -0700233 logging.warning('ignoring %s\n%s', p, e)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500234 continue
Don Garretta28be6d2016-06-16 18:09:35 -0700235 for p in FindSymbolFiles(tempdir, [ref.path]):
Mike Frysingerd41938e2014-02-10 06:37:55 -0500236 yield p
237
238 elif os.path.isdir(p):
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500239 for root, _, files in os.walk(p):
240 for f in files:
241 if f.endswith('.sym'):
Don Garretta28be6d2016-06-16 18:09:35 -0700242 # If p is '/tmp/foo' and filename is '/tmp/foo/bar/bar.sym',
243 # display_path = 'bar/bar.sym'
244 filename = os.path.join(root, f)
245 yield SymbolFile(display_path=filename[len(p):].lstrip('/'),
246 file_name=filename)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500247
248 elif IsTarball(p):
Ralph Nathan03047282015-03-23 11:09:32 -0700249 logging.info('processing files inside %s', p)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500250 tardir = tempfile.mkdtemp(dir=tempdir)
251 cache.Untar(os.path.realpath(p), tardir)
Don Garretta28be6d2016-06-16 18:09:35 -0700252 for p in FindSymbolFiles(tardir, [tardir]):
Mike Frysingerd41938e2014-02-10 06:37:55 -0500253 yield p
254
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500255 else:
Don Garretta28be6d2016-06-16 18:09:35 -0700256 yield SymbolFile(display_path=p, file_name=p)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500257
258
Don Garretta28be6d2016-06-16 18:09:35 -0700259def AdjustSymbolFileSize(symbol, tempdir, file_limit):
260 """Examine symbols files for size problems, and reduce if needed.
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500261
Don Garretta28be6d2016-06-16 18:09:35 -0700262 If the symbols size is too big, strip out the call frame info. The CFI
263 is unnecessary for 32bit x86 targets where the frame pointer is used (as
264 all of ours have) and it accounts for over half the size of the symbols
265 uploaded.
266
267 Stripped files will be created inside tempdir, and will be the callers
268 responsibility to clean up.
269
270 We also warn, if a symbols file is still too large after stripping.
Mike Frysinger5e6dd712014-03-07 22:21:17 -0500271
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500272 Args:
Don Garretta28be6d2016-06-16 18:09:35 -0700273 symbol: SymbolFile instance to be examined and modified as needed..
274 tempdir: A temporary directory we can create files in that the caller will
275 clean up.
276 file_limit: We only strip files which are larger than this limit.
277
278 Returns:
279 SymbolFile instance (original or modified as needed)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500280 """
Don Garretta28be6d2016-06-16 18:09:35 -0700281 file_size = symbol.FileSize()
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500282
Don Garretta28be6d2016-06-16 18:09:35 -0700283 if file_limit and symbol.FileSize() > file_limit:
284 with tempfile.NamedTemporaryFile(
285 prefix='upload_symbols', bufsize=0,
286 dir=tempdir, delete=False) as temp_sym_file:
287
288 temp_sym_file.writelines(
289 [x for x in open(symbol.file_name, 'rb').readlines()
290 if not x.startswith('STACK CFI')]
291 )
292
293 original_file_size = file_size
294 symbol.file_name = temp_sym_file.name
295 file_size = symbol.FileSize()
296
297 logging.warning('stripped CFI for %s reducing size %s > %s',
298 symbol.display_name, original_file_size, file_size)
299
300 # Hopefully the crash server will let it through. But it probably won't.
301 # Not sure what the best answer is in this case.
302 if file_size >= CRASH_SERVER_FILE_LIMIT:
303 logging.PrintBuildbotStepWarnings()
304 logging.warning('upload file %s is awfully large, risking rejection by '
305 'the symbol server (%s > %s)', symbol.display_path,
306 file_size, CRASH_SERVER_FILE_LIMIT)
307
308 return symbol
309
Don Garretta28be6d2016-06-16 18:09:35 -0700310
311def GetUploadTimeout(symbol):
312 """How long to wait for a specific file to upload to the crash server.
313
314 This is a function largely to make unittesting easier.
315
316 Args:
317 symbol: A SymbolFile instance.
318
319 Returns:
320 Timeout length (in seconds)
321 """
322 # Scale the timeout based on the filesize.
Mike Frysinger93e8ffa2019-07-03 20:24:18 -0400323 return max(symbol.FileSize() // UPLOAD_MIN_RATE, UPLOAD_MIN_TIMEOUT)
Don Garretta28be6d2016-06-16 18:09:35 -0700324
325
Mike Nichols90f7c152019-04-09 15:14:08 -0600326def ExecRequest(operator, url, timeout, api_key, **kwargs):
327 """Makes a web request with default timeout, returning the json result.
Don Garretta28be6d2016-06-16 18:09:35 -0700328
Mike Nichols90f7c152019-04-09 15:14:08 -0600329 This method will raise a requests.exceptions.HTTPError if the status
330 code is not 4XX, 5XX
331
332 Note: If you are using verbose logging it is entirely possible that the
333 subsystem will write your api key to the logs!
334
335 Args:
Mike Nichols649e6a82019-04-23 11:44:48 -0600336 operator: HTTP method.
337 url: Endpoint URL.
338 timeout: HTTP timeout for request.
339 api_key: Authentication key.
Mike Nichols90f7c152019-04-09 15:14:08 -0600340
341 Returns:
342 HTTP response content
343 """
344 resp = requests.request(operator, url,
345 params={'key': api_key},
346 headers={'User-agent': 'chromite.upload_symbols'},
347 timeout=timeout, **kwargs)
348 # Make sure we don't leak secret keys by accident.
349 if resp.status_code > 399:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400350 resp.url = resp.url.replace(urllib.parse.quote(api_key), 'XX-HIDDEN-XX')
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600351 logging.warning('Url: %s, Status: %s, response: "%s", in: %s',
352 resp.url, resp.status_code, resp.text, resp.elapsed)
353 elif resp.content:
Mike Nichols90f7c152019-04-09 15:14:08 -0600354 return resp.json()
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600355
Mike Nichols90f7c152019-04-09 15:14:08 -0600356 return {}
357
358
Mike Nichols137e82d2019-05-15 18:40:34 -0600359def FindDuplicates(symbols, status_url, api_key, timeout=DEDUPE_TIMEOUT):
360 """Check whether the symbol files have already been uploaded.
Mike Nichols649e6a82019-04-23 11:44:48 -0600361
362 Args:
Mike Nichols137e82d2019-05-15 18:40:34 -0600363 symbols: A iterable of SymbolFiles to be uploaded
Mike Nichols649e6a82019-04-23 11:44:48 -0600364 status_url: The crash URL to validate the file existence.
Mike Nichols649e6a82019-04-23 11:44:48 -0600365 api_key: Authentication key.
Mike Nichols137e82d2019-05-15 18:40:34 -0600366 timeout: HTTP timeout for request.
Mike Nichols649e6a82019-04-23 11:44:48 -0600367
368 Yields:
Mike Nichols137e82d2019-05-15 18:40:34 -0600369 All SymbolFiles from symbols, but duplicates have status updated to
370 DUPLICATE.
Mike Nichols649e6a82019-04-23 11:44:48 -0600371 """
Mike Nichols137e82d2019-05-15 18:40:34 -0600372 for batch in BatchGenerator(symbols, DEDUPE_LIMIT):
373 items = []
374 result = {}
375 for x in batch:
376 items.append({'debug_file': x.header.name,
377 'debug_id': x.header.id.replace('-', '')})
378 symbol_data = {'symbol_ids': items}
379 try:
380 result = ExecRequest('post', '%s/symbols:checkStatuses' %
381 status_url,
382 timeout,
383 api_key=api_key,
384 data=json.dumps(symbol_data))
385 except (requests.exceptions.HTTPError,
386 requests.exceptions.Timeout,
387 requests.exceptions.RequestException) as e:
388 logging.warning('could not identify duplicates: HTTP error: %s', e)
389 for b in batch:
390 b.status = SymbolFile.INITIAL
391 set_match = {'debugId': b.header.id.replace('-', ''),
392 'debugFile': b.header.name}
393 for cs_result in result.get('pairs', []):
Mike Frysingerca227ec2019-07-02 17:59:21 -0400394 if set_match == cs_result.get('symbolId'):
Mike Nichols137e82d2019-05-15 18:40:34 -0600395 if cs_result.get('status') == 'FOUND':
396 logging.debug('Found duplicate: %s', b.display_name)
397 b.status = SymbolFile.DUPLICATE
398 break
399 yield b
400
Mike Nichols649e6a82019-04-23 11:44:48 -0600401
Mike Nichols90f7c152019-04-09 15:14:08 -0600402def UploadSymbolFile(upload_url, symbol, api_key):
Mike Frysinger80de5012019-08-01 14:10:53 -0400403 """Upload a symbol file to the crash server, returning the status result.
Don Garretta28be6d2016-06-16 18:09:35 -0700404
405 Args:
406 upload_url: The crash URL to POST the |sym_file| to
407 symbol: A SymbolFile instance.
Mike Nichols90f7c152019-04-09 15:14:08 -0600408 api_key: Authentication key
Mike Frysinger80de5012019-08-01 14:10:53 -0400409 """
Mike Nichols90f7c152019-04-09 15:14:08 -0600410 timeout = GetUploadTimeout(symbol)
Mike Nichols137e82d2019-05-15 18:40:34 -0600411 upload = ExecRequest('post',
412 '%s/uploads:create' % upload_url, timeout, api_key)
Mike Nichols649e6a82019-04-23 11:44:48 -0600413
Mike Nichols137e82d2019-05-15 18:40:34 -0600414 if upload and 'uploadUrl' in upload.keys():
415 symbol_data = {'symbol_id':
416 {'debug_file': symbol.header.name,
417 'debug_id': symbol.header.id.replace('-', '')}
418 }
419 ExecRequest('put',
420 upload['uploadUrl'], timeout,
421 api_key=api_key,
422 data=open(symbol.file_name, 'r'))
423 ExecRequest('post',
424 '%s/uploads/%s:complete' % (
425 upload_url, upload['uploadKey']),
426 timeout, api_key=api_key,
427 # TODO(mikenichols): Validate product_name once it is added
428 # to the proto; currently unsupported.
429 data=json.dumps(symbol_data))
430 else:
431 raise requests.exceptions.HTTPError
Don Garretta28be6d2016-06-16 18:09:35 -0700432
433
Mike Nichols90f7c152019-04-09 15:14:08 -0600434def PerformSymbolsFileUpload(symbols, upload_url, api_key):
Don Garretta28be6d2016-06-16 18:09:35 -0700435 """Upload the symbols to the crash server
436
437 Args:
Don Garrettdeb2e032016-07-06 16:44:14 -0700438 symbols: An iterable of SymbolFiles to be uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700439 upload_url: URL of crash server to upload too.
Mike Nichols90f7c152019-04-09 15:14:08 -0600440 api_key: Authentication key.
Don Garretta28be6d2016-06-16 18:09:35 -0700441 failures: Tracker for total upload failures.
Don Garretta28be6d2016-06-16 18:09:35 -0700442
Don Garrettdeb2e032016-07-06 16:44:14 -0700443 Yields:
444 Each symbol from symbols, perhaps modified.
Don Garretta28be6d2016-06-16 18:09:35 -0700445 """
Don Garrettdeb2e032016-07-06 16:44:14 -0700446 failures = 0
Don Garretta28be6d2016-06-16 18:09:35 -0700447
Don Garrettdeb2e032016-07-06 16:44:14 -0700448 for s in symbols:
449 if (failures < MAX_TOTAL_ERRORS_FOR_RETRY and
450 s.status in (SymbolFile.INITIAL, SymbolFile.ERROR)):
451 # Keeps us from DoS-ing the symbol server.
452 time.sleep(SLEEP_DELAY)
453 logging.info('Uploading symbol_file: %s', s.display_path)
454 try:
455 # This command retries the upload multiple times with growing delays. We
456 # only consider the upload a failure if these retries fail.
Don Garrette1f47e92016-10-13 16:04:56 -0700457 def ShouldRetryUpload(exception):
Mike Nichols0abb8d32019-04-26 14:55:08 -0600458 if isinstance(exception, (requests.exceptions.RequestException,
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400459 IOError,
Mike Nichols0abb8d32019-04-26 14:55:08 -0600460 httplib.HTTPException, socket.error)):
461 logging.info('Request failed, retrying: %s', exception)
462 return True
463 return False
Don Garrette1f47e92016-10-13 16:04:56 -0700464
Don Garrett440944e2016-10-03 16:33:31 -0700465 with cros_build_lib.TimedSection() as timer:
Don Garrette1f47e92016-10-13 16:04:56 -0700466 retry_stats.RetryWithStats(
467 UPLOAD_STATS, ShouldRetryUpload, MAX_RETRIES,
Don Garrett440944e2016-10-03 16:33:31 -0700468 UploadSymbolFile,
Mike Nichols90f7c152019-04-09 15:14:08 -0600469 upload_url, s, api_key,
Luigi Semenzato5104f3c2016-10-12 12:37:42 -0700470 sleep=INITIAL_RETRY_DELAY,
471 log_all_retries=True)
Mike Nicholsb03b2c62019-05-02 18:46:04 -0600472 if s.status != SymbolFile.DUPLICATE:
473 logging.info('upload of %10i bytes took %s', s.FileSize(),
474 timer.delta)
475 s.status = SymbolFile.UPLOADED
Mike Nichols90f7c152019-04-09 15:14:08 -0600476 except (requests.exceptions.HTTPError,
477 requests.exceptions.Timeout,
478 requests.exceptions.RequestException) as e:
479 logging.warning('could not upload: %s: HTTP error: %s',
480 s.display_name, e)
Don Garrettdeb2e032016-07-06 16:44:14 -0700481 s.status = SymbolFile.ERROR
482 failures += 1
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400483 except (httplib.HTTPException, IOError, socket.error) as e:
Ryo Hashimoto45273f02017-03-16 17:45:56 +0900484 logging.warning('could not upload: %s: %s %s', s.display_name,
485 type(e).__name__, e)
Don Garrettdeb2e032016-07-06 16:44:14 -0700486 s.status = SymbolFile.ERROR
487 failures += 1
488
489 # We pass the symbol along, on both success and failure.
490 yield s
Don Garretta28be6d2016-06-16 18:09:35 -0700491
492
Don Garrettdeb2e032016-07-06 16:44:14 -0700493def ReportResults(symbols, failed_list):
Don Garretta28be6d2016-06-16 18:09:35 -0700494 """Log a summary of the symbol uploading.
495
496 This has the side effect of fully consuming the symbols iterator.
497
498 Args:
499 symbols: An iterator of SymbolFiles to be uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700500 failed_list: A filename at which to write out a list of our failed uploads.
Don Garrettdeb2e032016-07-06 16:44:14 -0700501
502 Returns:
503 The number of symbols not uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700504 """
505 upload_failures = []
506 result_counts = {
507 SymbolFile.INITIAL: 0,
508 SymbolFile.UPLOADED: 0,
509 SymbolFile.DUPLICATE: 0,
Don Garrettdeb2e032016-07-06 16:44:14 -0700510 SymbolFile.ERROR: 0,
Don Garretta28be6d2016-06-16 18:09:35 -0700511 }
512
513 for s in symbols:
514 result_counts[s.status] += 1
Don Garrettdeb2e032016-07-06 16:44:14 -0700515 if s.status in [SymbolFile.INITIAL, SymbolFile.ERROR]:
Don Garretta28be6d2016-06-16 18:09:35 -0700516 upload_failures.append(s)
517
Don Garrette1f47e92016-10-13 16:04:56 -0700518 # Report retry numbers.
519 _, _, retries = retry_stats.CategoryStats(UPLOAD_STATS)
520 if retries:
521 logging.warning('%d upload retries performed.', retries)
522
Don Garretta28be6d2016-06-16 18:09:35 -0700523 logging.info('Uploaded %(uploaded)d, Skipped %(duplicate)d duplicates.',
524 result_counts)
525
Chris Ching91908032016-09-27 16:55:33 -0600526 if result_counts[SymbolFile.ERROR]:
Don Garretta28be6d2016-06-16 18:09:35 -0700527 logging.PrintBuildbotStepWarnings()
Chris Ching91908032016-09-27 16:55:33 -0600528 logging.warning('%d non-recoverable upload errors',
529 result_counts[SymbolFile.ERROR])
530
531 if result_counts[SymbolFile.INITIAL]:
532 logging.PrintBuildbotStepWarnings()
533 logging.warning('%d upload(s) were skipped because of excessive errors',
Don Garrettdeb2e032016-07-06 16:44:14 -0700534 result_counts[SymbolFile.INITIAL])
Don Garretta28be6d2016-06-16 18:09:35 -0700535
536 if failed_list is not None:
537 with open(failed_list, 'w') as fl:
538 for s in upload_failures:
539 fl.write('%s\n' % s.display_path)
540
Don Garrettdeb2e032016-07-06 16:44:14 -0700541 return result_counts[SymbolFile.INITIAL] + result_counts[SymbolFile.ERROR]
542
Don Garretta28be6d2016-06-16 18:09:35 -0700543
Mike Nichols649e6a82019-04-23 11:44:48 -0600544def UploadSymbols(sym_paths, upload_url, failed_list=None,
Mike Nichols137e82d2019-05-15 18:40:34 -0600545 upload_limit=None, strip_cfi=None, timeout=None,
Mike Nichols90f7c152019-04-09 15:14:08 -0600546 api_key=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400547 """Upload all the generated symbols for |board| to the crash server
548
549 Args:
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500550 sym_paths: Specific symbol files (or dirs of sym files) to upload,
551 otherwise search |breakpad_dir|
Don Garretta28be6d2016-06-16 18:09:35 -0700552 upload_url: URL of crash server to upload too.
Don Garretta28be6d2016-06-16 18:09:35 -0700553 failed_list: A filename at which to write out a list of our failed uploads.
554 upload_limit: Integer listing how many files to upload. None for no limit.
555 strip_cfi: File size at which we strip out CFI data. None for no limit.
Mike Nichols137e82d2019-05-15 18:40:34 -0600556 timeout: HTTP timeout for request.
Mike Nichols90f7c152019-04-09 15:14:08 -0600557 api_key: A string based authentication key
Mike Frysinger1a736a82013-12-12 01:50:59 -0500558
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400559 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400560 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400561 """
Don Garrette1f47e92016-10-13 16:04:56 -0700562 retry_stats.SetupStats()
563
Don Garretta28be6d2016-06-16 18:09:35 -0700564 # Note: This method looks like each step of processing is performed
565 # sequentially for all SymbolFiles, but instead each step is a generator that
566 # produces the next iteration only when it's read. This means that (except for
567 # some batching) each SymbolFile goes through all of these steps before the
568 # next one is processed at all.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400569
Don Garretta28be6d2016-06-16 18:09:35 -0700570 # This is used to hold striped
571 with osutils.TempDir(prefix='upload_symbols.') as tempdir:
572 symbols = FindSymbolFiles(tempdir, sym_paths)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500573
Don Garrett1bc1e102016-07-06 17:06:10 -0700574 # Sort all of our symbols so the largest ones (probably the most important)
575 # are processed first.
576 symbols = list(symbols)
577 symbols.sort(key=lambda s: s.FileSize(), reverse=True)
578
Don Garretta28be6d2016-06-16 18:09:35 -0700579 if upload_limit is not None:
580 # Restrict symbols processed to the limit.
581 symbols = itertools.islice(symbols, None, upload_limit)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400582
Don Garretta28be6d2016-06-16 18:09:35 -0700583 # Strip CFI, if needed.
584 symbols = (AdjustSymbolFileSize(s, tempdir, strip_cfi) for s in symbols)
Mike Frysinger8ec8c502014-02-10 00:19:13 -0500585
Mike Nichols137e82d2019-05-15 18:40:34 -0600586 # Find duplicates via batch API
587 symbols = FindDuplicates(symbols, upload_url, api_key, timeout)
588
Don Garretta28be6d2016-06-16 18:09:35 -0700589 # Perform uploads
Mike Nichols90f7c152019-04-09 15:14:08 -0600590 symbols = PerformSymbolsFileUpload(symbols, upload_url, api_key)
Mike Frysingerd42e5f02014-03-14 11:19:37 -0400591
Don Garretta28be6d2016-06-16 18:09:35 -0700592 # Log the final results, and consume the symbols generator fully.
Don Garrettdeb2e032016-07-06 16:44:14 -0700593 failures = ReportResults(symbols, failed_list)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500594
Don Garrettdeb2e032016-07-06 16:44:14 -0700595 return failures
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400596
597
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400598def main(argv):
599 parser = commandline.ArgumentParser(description=__doc__)
600
Don Garretta28be6d2016-06-16 18:09:35 -0700601 # TODO: Make sym_paths, breakpad_root, and root exclusive.
602
Mike Frysingerd41938e2014-02-10 06:37:55 -0500603 parser.add_argument('sym_paths', type='path_or_uri', nargs='*', default=None,
604 help='symbol file or directory or URL or tarball')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400605 parser.add_argument('--board', default=None,
Don Garrett747cc4b2015-10-07 14:48:48 -0700606 help='Used to find default breakpad_root.')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400607 parser.add_argument('--breakpad_root', type='path', default=None,
Mike Frysingerc5597f22014-11-27 15:39:15 -0500608 help='full path to the breakpad symbol directory')
609 parser.add_argument('--root', type='path', default=None,
610 help='full path to the chroot dir')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400611 parser.add_argument('--official_build', action='store_true', default=False,
612 help='point to official symbol server')
Mike Frysinger38647542014-09-12 18:15:39 -0700613 parser.add_argument('--server', type=str, default=None,
614 help='URI for custom symbol server')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400615 parser.add_argument('--regenerate', action='store_true', default=False,
616 help='regenerate all symbols')
Don Garretta28be6d2016-06-16 18:09:35 -0700617 parser.add_argument('--upload-limit', type=int,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400618 help='only upload # number of symbols')
619 parser.add_argument('--strip_cfi', type=int,
Don Garretta28be6d2016-06-16 18:09:35 -0700620 default=DEFAULT_FILE_LIMIT,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400621 help='strip CFI data for files above this size')
Mike Frysinger7f9be142014-01-15 02:16:42 -0500622 parser.add_argument('--failed-list', type='path',
623 help='where to save a list of failed symbols')
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500624 parser.add_argument('--dedupe', action='store_true', default=False,
625 help='use the swarming service to avoid re-uploading')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400626 parser.add_argument('--yes', action='store_true', default=False,
627 help='answer yes to all prompts')
Don Garrett747cc4b2015-10-07 14:48:48 -0700628 parser.add_argument('--product_name', type=str, default='ChromeOS',
629 help='Produce Name for breakpad stats.')
Mike Nichols90f7c152019-04-09 15:14:08 -0600630 parser.add_argument('--api_key', type=str, default=None,
631 help='full path to the API key file')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400632
633 opts = parser.parse_args(argv)
Mike Frysinger90e49ca2014-01-14 14:42:07 -0500634 opts.Freeze()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400635
Don Garretta28be6d2016-06-16 18:09:35 -0700636 # Figure out the symbol files/directories to upload.
637 if opts.sym_paths:
638 sym_paths = opts.sym_paths
639 elif opts.breakpad_root:
640 sym_paths = [opts.breakpad_root]
641 elif opts.root:
642 if not opts.board:
Mike Frysinger8390dec2017-08-11 15:11:38 -0400643 cros_build_lib.Die('--board must be set if --root is used.')
Don Garretta28be6d2016-06-16 18:09:35 -0700644 breakpad_dir = cros_generate_breakpad_symbols.FindBreakpadDir(opts.board)
645 sym_paths = [os.path.join(opts.root, breakpad_dir.lstrip('/'))]
646 else:
Mike Frysinger8390dec2017-08-11 15:11:38 -0400647 cros_build_lib.Die('--sym_paths, --breakpad_root, or --root must be set.')
Don Garretta28be6d2016-06-16 18:09:35 -0700648
Don Garrett747cc4b2015-10-07 14:48:48 -0700649 if opts.sym_paths or opts.breakpad_root:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400650 if opts.regenerate:
Don Garrett747cc4b2015-10-07 14:48:48 -0700651 cros_build_lib.Die('--regenerate may not be used with specific files, '
652 'or breakpad_root')
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400653 else:
654 if opts.board is None:
655 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400656
Don Garretta28be6d2016-06-16 18:09:35 -0700657 # Figure out which crash server to upload too.
658 upload_url = opts.server
659 if not upload_url:
660 if opts.official_build:
661 upload_url = OFFICIAL_UPLOAD_URL
662 else:
663 logging.warning('unofficial builds upload to the staging server')
664 upload_url = STAGING_UPLOAD_URL
665
Mike Nichols90f7c152019-04-09 15:14:08 -0600666 # Set up the API key needed to authenticate to Crash server.
667 # Allow for a local key file for testing purposes.
668 if opts.api_key:
669 api_key_file = opts.api_key
670 else:
671 api_key_file = constants.CRASH_API_KEY
672
673 api_key = osutils.ReadFile(api_key_file)
674
Don Garretta28be6d2016-06-16 18:09:35 -0700675 # Confirm we really want the long upload.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400676 if not opts.yes:
Mike Frysingerc5de9602014-02-09 02:42:36 -0500677 prolog = '\n'.join(textwrap.wrap(textwrap.dedent("""
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400678 Uploading symbols for an entire Chromium OS build is really only
679 necessary for release builds and in a few cases for developers
680 to debug problems. It will take considerable time to run. For
681 developer debugging purposes, consider instead passing specific
682 files to upload.
Mike Frysingerc5de9602014-02-09 02:42:36 -0500683 """), 80)).strip()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400684 if not cros_build_lib.BooleanPrompt(
685 prompt='Are you sure you want to upload all build symbols',
Mike Frysingerc5de9602014-02-09 02:42:36 -0500686 default=False, prolog=prolog):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400687 cros_build_lib.Die('better safe than sorry')
688
689 ret = 0
Don Garretta28be6d2016-06-16 18:09:35 -0700690
691 # Regenerate symbols from binaries.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400692 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400693 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
694 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400695
Don Garretta28be6d2016-06-16 18:09:35 -0700696 # Do the upload.
697 ret += UploadSymbols(
698 sym_paths=sym_paths,
699 upload_url=upload_url,
Don Garretta28be6d2016-06-16 18:09:35 -0700700 failed_list=opts.failed_list,
701 upload_limit=opts.upload_limit,
Mike Nichols90f7c152019-04-09 15:14:08 -0600702 strip_cfi=opts.strip_cfi,
703 api_key=api_key)
Don Garretta28be6d2016-06-16 18:09:35 -0700704
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400705 if ret:
Ralph Nathan59900422015-03-24 10:41:17 -0700706 logging.error('encountered %i problem(s)', ret)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400707 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
708 # return 0 in case we are a multiple of the mask.
Don Garretta28be6d2016-06-16 18:09:35 -0700709 return 1