blob: b374b01fadf6d05018d7152ca8c732a2bb80f448 [file] [log] [blame]
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -04001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Upload all debug symbols required for crash reporting purposes.
6
7This script need only be used to upload release builds symbols or to debug
8crashes on non-release builds (in which case try to only upload the symbols
Mike Frysinger02e1e072013-11-10 22:11:34 -05009for those executables involved).
10"""
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040011
Mike Frysingere852b072021-05-21 12:39:03 -040012import http.client
Don Garretta28be6d2016-06-16 18:09:35 -070013import itertools
Mike Nichols90f7c152019-04-09 15:14:08 -060014import json
Chris McDonaldb55b7032021-06-17 16:41:32 -060015import logging
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040016import os
Mike Frysingerfd355652014-01-23 02:57:48 -050017import socket
Mike Frysingerc5597f22014-11-27 15:39:15 -050018import sys
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040019import tempfile
Mike Frysinger71bf7862021-02-12 07:38:57 -050020import textwrap
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040021import time
Mike Frysingere852b072021-05-21 12:39:03 -040022import urllib.parse
Mike Frysinger6db648e2018-07-24 19:57:58 -040023
Chris McDonaldb55b7032021-06-17 16:41:32 -060024from chromite.third_party import requests
25
26from chromite.cbuildbot import cbuildbot_alerts
Mike Frysingerd41938e2014-02-10 06:37:55 -050027from chromite.lib import cache
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040028from chromite.lib import commandline
Mike Frysinger38002522019-10-13 23:34:35 -040029from chromite.lib import constants
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040030from chromite.lib import cros_build_lib
Mike Frysingerd41938e2014-02-10 06:37:55 -050031from chromite.lib import gs
32from chromite.lib import osutils
Gilad Arnold83233ed2015-05-08 12:12:13 -070033from chromite.lib import path_util
Don Garrette1f47e92016-10-13 16:04:56 -070034from chromite.lib import retry_stats
Mike Frysinger69cb41d2013-08-11 20:08:19 -040035from chromite.scripts import cros_generate_breakpad_symbols
Alex Klein18ef1212021-10-14 12:49:02 -060036from chromite.utils import timer
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040037
Mike Frysinger2688ef62020-02-16 00:00:46 -050038
Mike Frysinger0c0efa22014-02-09 23:32:23 -050039# Needs to be after chromite imports.
Mike Frysingerc5597f22014-11-27 15:39:15 -050040# We don't want to import the general keyring module as that will implicitly
41# try to import & connect to a dbus server. That's a waste of time.
42sys.modules['keyring'] = None
Mike Frysinger0c0efa22014-02-09 23:32:23 -050043
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040044
45# URLs used for uploading symbols.
Mike Nichols90f7c152019-04-09 15:14:08 -060046OFFICIAL_UPLOAD_URL = 'https://prod-crashsymbolcollector-pa.googleapis.com/v1'
47STAGING_UPLOAD_URL = 'https://staging-crashsymbolcollector-pa.googleapis.com/v1'
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040048
Ian Barkley-Yeung22ba8122020-02-05 15:39:02 -080049# The crash server rejects files that are bigger than 1GB.
50CRASH_SERVER_FILE_LIMIT = 1024 * 1024 * 1024
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040051# Give ourselves a little breathing room from what the server expects.
52DEFAULT_FILE_LIMIT = CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024)
53
54
Mike Frysinger0c0efa22014-02-09 23:32:23 -050055# The batch limit when talking to the dedup server. We avoid sending one at a
56# time as the round trip overhead will dominate. Conversely, we avoid sending
57# all at once so we can start uploading symbols asap -- the symbol server is a
58# bit slow and will take longer than anything else.
Mike Frysinger0c0efa22014-02-09 23:32:23 -050059DEDUPE_LIMIT = 100
60
61# How long to wait for the server to respond with the results. Note that the
62# larger the limit above, the larger this will need to be. So we give it ~1
63# second per item max.
64DEDUPE_TIMEOUT = DEDUPE_LIMIT
65
Don Garretta28be6d2016-06-16 18:09:35 -070066# How long to wait for the notification to finish (in seconds).
67DEDUPE_NOTIFY_TIMEOUT = 240
Mike Frysinger4dd462e2014-04-30 16:21:51 -040068
Mike Frysinger71046662014-09-12 18:15:15 -070069# The minimum average rate (in bytes per second) that we expect to maintain
70# when uploading symbols. This has to allow for symbols that are up to
71# CRASH_SERVER_FILE_LIMIT in size.
Mike Frysinger93e8ffa2019-07-03 20:24:18 -040072UPLOAD_MIN_RATE = CRASH_SERVER_FILE_LIMIT // (30 * 60)
Mike Frysinger71046662014-09-12 18:15:15 -070073
74# The lowest timeout (in seconds) we'll allow. If the server is overloaded,
75# then there might be a delay in setting up the connection, not just with the
76# transfer. So even a small file might need a larger value.
Ryo Hashimotoc0049372017-02-16 18:50:00 +090077UPLOAD_MIN_TIMEOUT = 5 * 60
Mike Frysingercd78a082013-06-26 17:13:04 -040078
79
Don Garrett7a793092016-07-06 16:50:27 -070080# Sleep for 500ms in between uploads to avoid DoS'ing symbol server.
81SLEEP_DELAY = 0.5
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040082
83
84# Number of seconds to wait before retrying an upload. The delay will double
85# for each subsequent retry of the same symbol file.
86INITIAL_RETRY_DELAY = 1
87
88# Allow up to 7 attempts to upload a symbol file (total delay may be
89# 1+2+4+8+16+32=63 seconds).
Mike Nichols2a6f86f2020-08-18 15:56:07 -060090MAX_RETRIES = 5
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040091
Mike Frysingereb753bf2013-11-22 16:05:35 -050092# Number of total errors, before uploads are no longer attempted.
93# This is used to avoid lots of errors causing unreasonable delays.
Mike Nichols2a6f86f2020-08-18 15:56:07 -060094MAX_TOTAL_ERRORS_FOR_RETRY = 6
Mike Frysingereb753bf2013-11-22 16:05:35 -050095
Don Garrette1f47e92016-10-13 16:04:56 -070096# Category to use for collection upload retry stats.
97UPLOAD_STATS = 'UPLOAD'
98
Don Garretta28be6d2016-06-16 18:09:35 -070099
Don Garretta28be6d2016-06-16 18:09:35 -0700100def BatchGenerator(iterator, batch_size):
101 """Given an iterator, break into lists of size batch_size.
Fang Dengba680462015-08-16 20:34:11 -0700102
Don Garretta28be6d2016-06-16 18:09:35 -0700103 The result is a generator, that will only read in as many inputs as needed for
104 the current batch. The final result can be smaller than batch_size.
Mike Frysinger0a2fd922014-09-12 20:23:42 -0700105 """
Don Garretta28be6d2016-06-16 18:09:35 -0700106 batch = []
107 for i in iterator:
108 batch.append(i)
109 if len(batch) >= batch_size:
110 yield batch
111 batch = []
Mike Frysinger0a2fd922014-09-12 20:23:42 -0700112
Don Garretta28be6d2016-06-16 18:09:35 -0700113 if batch:
114 # if there was anything left in the final batch, yield it.
115 yield batch
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500116
117
Mike Frysingerd41938e2014-02-10 06:37:55 -0500118def IsTarball(path):
119 """Guess if this is a tarball based on the filename."""
120 parts = path.split('.')
121 if len(parts) <= 1:
122 return False
123
124 if parts[-1] == 'tar':
125 return True
126
127 if parts[-2] == 'tar':
128 return parts[-1] in ('bz2', 'gz', 'xz')
129
130 return parts[-1] in ('tbz2', 'tbz', 'tgz', 'txz')
131
132
Don Garretta28be6d2016-06-16 18:09:35 -0700133class SymbolFile(object):
134 """This class represents the state of a symbol file during processing.
135
Mike Frysinger309f6ab2019-09-22 13:33:54 -0400136 Attributes:
Don Garretta28be6d2016-06-16 18:09:35 -0700137 display_path: Name of symbol file that should be consistent between builds.
138 file_name: Transient path of the symbol file.
139 header: ReadSymsHeader output. Dict with assorted meta-data.
140 status: INITIAL, DUPLICATE, or UPLOADED based on status of processing.
141 dedupe_item: None or instance of DedupeItem for this symbol file.
142 dedupe_push_state: Opaque value to return to dedupe code for file.
143 display_name: Read only friendly (short) file name for logging.
144 file_size: Read only size of the symbol file.
145 """
146 INITIAL = 'initial'
147 DUPLICATE = 'duplicate'
148 UPLOADED = 'uploaded'
Don Garrettdeb2e032016-07-06 16:44:14 -0700149 ERROR = 'error'
Don Garretta28be6d2016-06-16 18:09:35 -0700150
151 def __init__(self, display_path, file_name):
152 """An instance of this class represents a symbol file over time.
153
154 Args:
155 display_path: A unique/persistent between builds name to present to the
156 crash server. It is the file name, relative to where it
157 came from (tarball, breakpad dir, etc).
158 file_name: A the current location of the symbol file.
159 """
160 self.display_path = display_path
161 self.file_name = file_name
162 self.header = cros_generate_breakpad_symbols.ReadSymsHeader(file_name)
163 self.status = SymbolFile.INITIAL
Don Garretta28be6d2016-06-16 18:09:35 -0700164
165 @property
166 def display_name(self):
167 return os.path.basename(self.display_path)
168
169 def FileSize(self):
170 return os.path.getsize(self.file_name)
171
172
Don Garretta28be6d2016-06-16 18:09:35 -0700173def FindSymbolFiles(tempdir, paths):
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500174 """Locate symbol files in |paths|
175
Don Garretta28be6d2016-06-16 18:09:35 -0700176 This returns SymbolFile objects that contain file references which are valid
177 after this exits. Those files may exist externally, or be created in the
178 tempdir (say, when expanding tarballs). The caller must not consider
179 SymbolFile's valid after tempdir is cleaned up.
180
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500181 Args:
Don Garretta28be6d2016-06-16 18:09:35 -0700182 tempdir: Path to use for temporary files.
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500183 paths: A list of input paths to walk. Files are returned w/out any checks.
Mike Frysingerd41938e2014-02-10 06:37:55 -0500184 Dirs are searched for files that end in ".sym". Urls are fetched and then
185 processed. Tarballs are unpacked and walked.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500186
Don Garretta28be6d2016-06-16 18:09:35 -0700187 Yields:
188 A SymbolFile for every symbol file found in paths.
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500189 """
Gilad Arnold83233ed2015-05-08 12:12:13 -0700190 cache_dir = path_util.GetCacheDir()
Mike Frysingere847efd2015-01-08 03:57:24 -0500191 common_path = os.path.join(cache_dir, constants.COMMON_CACHE)
192 tar_cache = cache.TarballCache(common_path)
193
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500194 for p in paths:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400195 o = urllib.parse.urlparse(p)
Mike Frysinger27e21b72018-07-12 14:20:21 -0400196 if o.scheme:
Mike Frysingerd41938e2014-02-10 06:37:55 -0500197 # Support globs of filenames.
198 ctx = gs.GSContext()
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400199 for gspath in ctx.LS(p):
200 logging.info('processing files inside %s', gspath)
201 o = urllib.parse.urlparse(gspath)
Mike Frysinger27e21b72018-07-12 14:20:21 -0400202 key = ('%s%s' % (o.netloc, o.path)).split('/')
Mike Frysingerd41938e2014-02-10 06:37:55 -0500203 # The common cache will not be LRU, removing the need to hold a read
204 # lock on the cached gsutil.
205 ref = tar_cache.Lookup(key)
206 try:
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400207 ref.SetDefault(gspath)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500208 except cros_build_lib.RunCommandError as e:
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400209 logging.warning('ignoring %s\n%s', gspath, e)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500210 continue
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400211 for sym in FindSymbolFiles(tempdir, [ref.path]):
212 yield sym
Mike Frysingerd41938e2014-02-10 06:37:55 -0500213
214 elif os.path.isdir(p):
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500215 for root, _, files in os.walk(p):
216 for f in files:
217 if f.endswith('.sym'):
Don Garretta28be6d2016-06-16 18:09:35 -0700218 # If p is '/tmp/foo' and filename is '/tmp/foo/bar/bar.sym',
219 # display_path = 'bar/bar.sym'
220 filename = os.path.join(root, f)
221 yield SymbolFile(display_path=filename[len(p):].lstrip('/'),
222 file_name=filename)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500223
224 elif IsTarball(p):
Ralph Nathan03047282015-03-23 11:09:32 -0700225 logging.info('processing files inside %s', p)
Mike Frysingerd41938e2014-02-10 06:37:55 -0500226 tardir = tempfile.mkdtemp(dir=tempdir)
227 cache.Untar(os.path.realpath(p), tardir)
Mike Frysinger8ab15bb2019-09-18 17:24:36 -0400228 for sym in FindSymbolFiles(tardir, [tardir]):
229 yield sym
Mike Frysingerd41938e2014-02-10 06:37:55 -0500230
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500231 else:
Don Garretta28be6d2016-06-16 18:09:35 -0700232 yield SymbolFile(display_path=p, file_name=p)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500233
234
Don Garretta28be6d2016-06-16 18:09:35 -0700235def AdjustSymbolFileSize(symbol, tempdir, file_limit):
236 """Examine symbols files for size problems, and reduce if needed.
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500237
Don Garretta28be6d2016-06-16 18:09:35 -0700238 If the symbols size is too big, strip out the call frame info. The CFI
239 is unnecessary for 32bit x86 targets where the frame pointer is used (as
240 all of ours have) and it accounts for over half the size of the symbols
241 uploaded.
242
243 Stripped files will be created inside tempdir, and will be the callers
244 responsibility to clean up.
245
246 We also warn, if a symbols file is still too large after stripping.
Mike Frysinger5e6dd712014-03-07 22:21:17 -0500247
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500248 Args:
Don Garretta28be6d2016-06-16 18:09:35 -0700249 symbol: SymbolFile instance to be examined and modified as needed..
250 tempdir: A temporary directory we can create files in that the caller will
251 clean up.
252 file_limit: We only strip files which are larger than this limit.
253
254 Returns:
255 SymbolFile instance (original or modified as needed)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500256 """
Don Garretta28be6d2016-06-16 18:09:35 -0700257 file_size = symbol.FileSize()
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500258
Don Garretta28be6d2016-06-16 18:09:35 -0700259 if file_limit and symbol.FileSize() > file_limit:
Mike Frysinger59babdb2019-09-06 06:25:50 -0400260 with cros_build_lib.UnbufferedNamedTemporaryFile(
261 prefix='upload_symbols',
Don Garretta28be6d2016-06-16 18:09:35 -0700262 dir=tempdir, delete=False) as temp_sym_file:
263
Mike Frysingerac75f602019-11-14 23:18:51 -0500264 with open(symbol.file_name, 'rb') as fp:
265 temp_sym_file.writelines(
266 x for x in fp.readlines() if not x.startswith(b'STACK CFI')
267 )
Don Garretta28be6d2016-06-16 18:09:35 -0700268
269 original_file_size = file_size
270 symbol.file_name = temp_sym_file.name
271 file_size = symbol.FileSize()
272
273 logging.warning('stripped CFI for %s reducing size %s > %s',
274 symbol.display_name, original_file_size, file_size)
275
276 # Hopefully the crash server will let it through. But it probably won't.
277 # Not sure what the best answer is in this case.
278 if file_size >= CRASH_SERVER_FILE_LIMIT:
Chris McDonaldb55b7032021-06-17 16:41:32 -0600279 cbuildbot_alerts.PrintBuildbotStepWarnings()
Don Garretta28be6d2016-06-16 18:09:35 -0700280 logging.warning('upload file %s is awfully large, risking rejection by '
281 'the symbol server (%s > %s)', symbol.display_path,
282 file_size, CRASH_SERVER_FILE_LIMIT)
283
284 return symbol
285
Don Garretta28be6d2016-06-16 18:09:35 -0700286
287def GetUploadTimeout(symbol):
288 """How long to wait for a specific file to upload to the crash server.
289
290 This is a function largely to make unittesting easier.
291
292 Args:
293 symbol: A SymbolFile instance.
294
295 Returns:
296 Timeout length (in seconds)
297 """
298 # Scale the timeout based on the filesize.
Mike Frysinger93e8ffa2019-07-03 20:24:18 -0400299 return max(symbol.FileSize() // UPLOAD_MIN_RATE, UPLOAD_MIN_TIMEOUT)
Don Garretta28be6d2016-06-16 18:09:35 -0700300
301
Mike Nichols90f7c152019-04-09 15:14:08 -0600302def ExecRequest(operator, url, timeout, api_key, **kwargs):
303 """Makes a web request with default timeout, returning the json result.
Don Garretta28be6d2016-06-16 18:09:35 -0700304
Mike Nichols90f7c152019-04-09 15:14:08 -0600305 This method will raise a requests.exceptions.HTTPError if the status
306 code is not 4XX, 5XX
307
308 Note: If you are using verbose logging it is entirely possible that the
309 subsystem will write your api key to the logs!
310
311 Args:
Mike Nichols649e6a82019-04-23 11:44:48 -0600312 operator: HTTP method.
313 url: Endpoint URL.
314 timeout: HTTP timeout for request.
315 api_key: Authentication key.
Mike Nichols90f7c152019-04-09 15:14:08 -0600316
317 Returns:
318 HTTP response content
319 """
320 resp = requests.request(operator, url,
321 params={'key': api_key},
322 headers={'User-agent': 'chromite.upload_symbols'},
323 timeout=timeout, **kwargs)
324 # Make sure we don't leak secret keys by accident.
325 if resp.status_code > 399:
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400326 resp.url = resp.url.replace(urllib.parse.quote(api_key), 'XX-HIDDEN-XX')
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600327 logging.warning('Url: %s, Status: %s, response: "%s", in: %s',
328 resp.url, resp.status_code, resp.text, resp.elapsed)
329 elif resp.content:
Mike Nichols90f7c152019-04-09 15:14:08 -0600330 return resp.json()
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600331
Mike Nichols90f7c152019-04-09 15:14:08 -0600332 return {}
333
334
Mike Nichols137e82d2019-05-15 18:40:34 -0600335def FindDuplicates(symbols, status_url, api_key, timeout=DEDUPE_TIMEOUT):
336 """Check whether the symbol files have already been uploaded.
Mike Nichols649e6a82019-04-23 11:44:48 -0600337
338 Args:
Mike Nichols137e82d2019-05-15 18:40:34 -0600339 symbols: A iterable of SymbolFiles to be uploaded
Mike Nichols649e6a82019-04-23 11:44:48 -0600340 status_url: The crash URL to validate the file existence.
Mike Nichols649e6a82019-04-23 11:44:48 -0600341 api_key: Authentication key.
Mike Nichols137e82d2019-05-15 18:40:34 -0600342 timeout: HTTP timeout for request.
Mike Nichols649e6a82019-04-23 11:44:48 -0600343
344 Yields:
Mike Nichols137e82d2019-05-15 18:40:34 -0600345 All SymbolFiles from symbols, but duplicates have status updated to
346 DUPLICATE.
Mike Nichols649e6a82019-04-23 11:44:48 -0600347 """
Mike Nichols137e82d2019-05-15 18:40:34 -0600348 for batch in BatchGenerator(symbols, DEDUPE_LIMIT):
349 items = []
350 result = {}
351 for x in batch:
352 items.append({'debug_file': x.header.name,
353 'debug_id': x.header.id.replace('-', '')})
354 symbol_data = {'symbol_ids': items}
355 try:
356 result = ExecRequest('post', '%s/symbols:checkStatuses' %
357 status_url,
358 timeout,
359 api_key=api_key,
360 data=json.dumps(symbol_data))
Mike Frysingerc7d164a2019-11-06 17:09:45 -0500361 except requests.exceptions.RequestException as e:
Mike Nichols137e82d2019-05-15 18:40:34 -0600362 logging.warning('could not identify duplicates: HTTP error: %s', e)
363 for b in batch:
364 b.status = SymbolFile.INITIAL
365 set_match = {'debugId': b.header.id.replace('-', ''),
366 'debugFile': b.header.name}
367 for cs_result in result.get('pairs', []):
Mike Frysingerca227ec2019-07-02 17:59:21 -0400368 if set_match == cs_result.get('symbolId'):
Mike Nichols137e82d2019-05-15 18:40:34 -0600369 if cs_result.get('status') == 'FOUND':
370 logging.debug('Found duplicate: %s', b.display_name)
371 b.status = SymbolFile.DUPLICATE
372 break
373 yield b
374
Mike Nichols649e6a82019-04-23 11:44:48 -0600375
Mike Nichols90f7c152019-04-09 15:14:08 -0600376def UploadSymbolFile(upload_url, symbol, api_key):
Mike Frysinger80de5012019-08-01 14:10:53 -0400377 """Upload a symbol file to the crash server, returning the status result.
Don Garretta28be6d2016-06-16 18:09:35 -0700378
379 Args:
380 upload_url: The crash URL to POST the |sym_file| to
381 symbol: A SymbolFile instance.
Mike Nichols90f7c152019-04-09 15:14:08 -0600382 api_key: Authentication key
Mike Frysinger80de5012019-08-01 14:10:53 -0400383 """
Mike Nichols90f7c152019-04-09 15:14:08 -0600384 timeout = GetUploadTimeout(symbol)
Mike Nichols2a6f86f2020-08-18 15:56:07 -0600385 logging.debug('Executing post to uploads:create: %s', symbol.display_name)
Mike Nichols137e82d2019-05-15 18:40:34 -0600386 upload = ExecRequest('post',
387 '%s/uploads:create' % upload_url, timeout, api_key)
Mike Nichols649e6a82019-04-23 11:44:48 -0600388
Mike Nichols137e82d2019-05-15 18:40:34 -0600389 if upload and 'uploadUrl' in upload.keys():
390 symbol_data = {'symbol_id':
391 {'debug_file': symbol.header.name,
392 'debug_id': symbol.header.id.replace('-', '')}
393 }
Mike Frysingerac75f602019-11-14 23:18:51 -0500394 with open(symbol.file_name, 'r') as fp:
Mike Nichols2a6f86f2020-08-18 15:56:07 -0600395 logging.debug('Executing put to uploadUrl: %s', symbol.display_name)
Mike Frysingerac75f602019-11-14 23:18:51 -0500396 ExecRequest('put',
397 upload['uploadUrl'], timeout,
398 api_key=api_key,
Mike Nicholsb99f86c2020-02-24 18:36:16 -0700399 data=fp.read())
Mike Nichols2a6f86f2020-08-18 15:56:07 -0600400 logging.debug('Executing post to uploads/complete: %s', symbol.display_name)
Mike Nichols137e82d2019-05-15 18:40:34 -0600401 ExecRequest('post',
402 '%s/uploads/%s:complete' % (
403 upload_url, upload['uploadKey']),
404 timeout, api_key=api_key,
405 # TODO(mikenichols): Validate product_name once it is added
406 # to the proto; currently unsupported.
407 data=json.dumps(symbol_data))
408 else:
409 raise requests.exceptions.HTTPError
Don Garretta28be6d2016-06-16 18:09:35 -0700410
411
Mike Nichols90f7c152019-04-09 15:14:08 -0600412def PerformSymbolsFileUpload(symbols, upload_url, api_key):
Don Garretta28be6d2016-06-16 18:09:35 -0700413 """Upload the symbols to the crash server
414
415 Args:
Don Garrettdeb2e032016-07-06 16:44:14 -0700416 symbols: An iterable of SymbolFiles to be uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700417 upload_url: URL of crash server to upload too.
Mike Nichols90f7c152019-04-09 15:14:08 -0600418 api_key: Authentication key.
Don Garretta28be6d2016-06-16 18:09:35 -0700419 failures: Tracker for total upload failures.
Don Garretta28be6d2016-06-16 18:09:35 -0700420
Don Garrettdeb2e032016-07-06 16:44:14 -0700421 Yields:
422 Each symbol from symbols, perhaps modified.
Don Garretta28be6d2016-06-16 18:09:35 -0700423 """
Don Garrettdeb2e032016-07-06 16:44:14 -0700424 failures = 0
Mike Nichols2a6f86f2020-08-18 15:56:07 -0600425 # Failures are figured per request, therefore each HTTP failure
426 # counts against the max. This means that the process will exit
427 # at the point when HTTP errors hit the defined limit.
Don Garrettdeb2e032016-07-06 16:44:14 -0700428 for s in symbols:
429 if (failures < MAX_TOTAL_ERRORS_FOR_RETRY and
430 s.status in (SymbolFile.INITIAL, SymbolFile.ERROR)):
431 # Keeps us from DoS-ing the symbol server.
432 time.sleep(SLEEP_DELAY)
433 logging.info('Uploading symbol_file: %s', s.display_path)
434 try:
435 # This command retries the upload multiple times with growing delays. We
436 # only consider the upload a failure if these retries fail.
Don Garrette1f47e92016-10-13 16:04:56 -0700437 def ShouldRetryUpload(exception):
Mike Nichols0abb8d32019-04-26 14:55:08 -0600438 if isinstance(exception, (requests.exceptions.RequestException,
Mike Frysinger3dcacee2019-08-23 17:09:11 -0400439 IOError,
Mike Frysingere852b072021-05-21 12:39:03 -0400440 http.client.HTTPException, socket.error)):
Mike Nichols0abb8d32019-04-26 14:55:08 -0600441 logging.info('Request failed, retrying: %s', exception)
442 return True
443 return False
Don Garrette1f47e92016-10-13 16:04:56 -0700444
Alex Klein18ef1212021-10-14 12:49:02 -0600445 with timer.Timer() as t:
Don Garrette1f47e92016-10-13 16:04:56 -0700446 retry_stats.RetryWithStats(
447 UPLOAD_STATS, ShouldRetryUpload, MAX_RETRIES,
Don Garrett440944e2016-10-03 16:33:31 -0700448 UploadSymbolFile,
Mike Nichols90f7c152019-04-09 15:14:08 -0600449 upload_url, s, api_key,
Luigi Semenzato5104f3c2016-10-12 12:37:42 -0700450 sleep=INITIAL_RETRY_DELAY,
451 log_all_retries=True)
Mike Nicholsb03b2c62019-05-02 18:46:04 -0600452 if s.status != SymbolFile.DUPLICATE:
Mike Nichols2a6f86f2020-08-18 15:56:07 -0600453 logging.info('upload of %s with size %10i bytes took %s',
Alex Klein18ef1212021-10-14 12:49:02 -0600454 s.display_name, s.FileSize(), t)
Mike Nicholsb03b2c62019-05-02 18:46:04 -0600455 s.status = SymbolFile.UPLOADED
Mike Frysingerc7d164a2019-11-06 17:09:45 -0500456 except requests.exceptions.RequestException as e:
Mike Nichols90f7c152019-04-09 15:14:08 -0600457 logging.warning('could not upload: %s: HTTP error: %s',
458 s.display_name, e)
Don Garrettdeb2e032016-07-06 16:44:14 -0700459 s.status = SymbolFile.ERROR
460 failures += 1
Mike Frysingere852b072021-05-21 12:39:03 -0400461 except (http.client.HTTPException, OSError) as e:
Ryo Hashimoto45273f02017-03-16 17:45:56 +0900462 logging.warning('could not upload: %s: %s %s', s.display_name,
463 type(e).__name__, e)
Don Garrettdeb2e032016-07-06 16:44:14 -0700464 s.status = SymbolFile.ERROR
465 failures += 1
466
467 # We pass the symbol along, on both success and failure.
468 yield s
Don Garretta28be6d2016-06-16 18:09:35 -0700469
470
Don Garrettdeb2e032016-07-06 16:44:14 -0700471def ReportResults(symbols, failed_list):
Don Garretta28be6d2016-06-16 18:09:35 -0700472 """Log a summary of the symbol uploading.
473
474 This has the side effect of fully consuming the symbols iterator.
475
476 Args:
477 symbols: An iterator of SymbolFiles to be uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700478 failed_list: A filename at which to write out a list of our failed uploads.
Don Garrettdeb2e032016-07-06 16:44:14 -0700479
480 Returns:
481 The number of symbols not uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700482 """
483 upload_failures = []
484 result_counts = {
485 SymbolFile.INITIAL: 0,
486 SymbolFile.UPLOADED: 0,
487 SymbolFile.DUPLICATE: 0,
Don Garrettdeb2e032016-07-06 16:44:14 -0700488 SymbolFile.ERROR: 0,
Don Garretta28be6d2016-06-16 18:09:35 -0700489 }
490
491 for s in symbols:
492 result_counts[s.status] += 1
Don Garrettdeb2e032016-07-06 16:44:14 -0700493 if s.status in [SymbolFile.INITIAL, SymbolFile.ERROR]:
Don Garretta28be6d2016-06-16 18:09:35 -0700494 upload_failures.append(s)
495
Don Garrette1f47e92016-10-13 16:04:56 -0700496 # Report retry numbers.
497 _, _, retries = retry_stats.CategoryStats(UPLOAD_STATS)
498 if retries:
499 logging.warning('%d upload retries performed.', retries)
500
Don Garretta28be6d2016-06-16 18:09:35 -0700501 logging.info('Uploaded %(uploaded)d, Skipped %(duplicate)d duplicates.',
502 result_counts)
503
Chris Ching91908032016-09-27 16:55:33 -0600504 if result_counts[SymbolFile.ERROR]:
Chris McDonaldb55b7032021-06-17 16:41:32 -0600505 cbuildbot_alerts.PrintBuildbotStepWarnings()
Chris Ching91908032016-09-27 16:55:33 -0600506 logging.warning('%d non-recoverable upload errors',
507 result_counts[SymbolFile.ERROR])
508
509 if result_counts[SymbolFile.INITIAL]:
Chris McDonaldb55b7032021-06-17 16:41:32 -0600510 cbuildbot_alerts.PrintBuildbotStepWarnings()
Chris Ching91908032016-09-27 16:55:33 -0600511 logging.warning('%d upload(s) were skipped because of excessive errors',
Don Garrettdeb2e032016-07-06 16:44:14 -0700512 result_counts[SymbolFile.INITIAL])
Don Garretta28be6d2016-06-16 18:09:35 -0700513
514 if failed_list is not None:
515 with open(failed_list, 'w') as fl:
516 for s in upload_failures:
517 fl.write('%s\n' % s.display_path)
518
Don Garrettdeb2e032016-07-06 16:44:14 -0700519 return result_counts[SymbolFile.INITIAL] + result_counts[SymbolFile.ERROR]
520
Don Garretta28be6d2016-06-16 18:09:35 -0700521
Mike Nichols649e6a82019-04-23 11:44:48 -0600522def UploadSymbols(sym_paths, upload_url, failed_list=None,
Mike Nichols137e82d2019-05-15 18:40:34 -0600523 upload_limit=None, strip_cfi=None, timeout=None,
Mike Nichols90f7c152019-04-09 15:14:08 -0600524 api_key=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400525 """Upload all the generated symbols for |board| to the crash server
526
527 Args:
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500528 sym_paths: Specific symbol files (or dirs of sym files) to upload,
529 otherwise search |breakpad_dir|
Don Garretta28be6d2016-06-16 18:09:35 -0700530 upload_url: URL of crash server to upload too.
Don Garretta28be6d2016-06-16 18:09:35 -0700531 failed_list: A filename at which to write out a list of our failed uploads.
532 upload_limit: Integer listing how many files to upload. None for no limit.
533 strip_cfi: File size at which we strip out CFI data. None for no limit.
Mike Nichols137e82d2019-05-15 18:40:34 -0600534 timeout: HTTP timeout for request.
Mike Nichols90f7c152019-04-09 15:14:08 -0600535 api_key: A string based authentication key
Mike Frysinger1a736a82013-12-12 01:50:59 -0500536
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400537 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400538 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400539 """
Don Garrette1f47e92016-10-13 16:04:56 -0700540 retry_stats.SetupStats()
541
Don Garretta28be6d2016-06-16 18:09:35 -0700542 # Note: This method looks like each step of processing is performed
543 # sequentially for all SymbolFiles, but instead each step is a generator that
544 # produces the next iteration only when it's read. This means that (except for
545 # some batching) each SymbolFile goes through all of these steps before the
546 # next one is processed at all.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400547
Don Garretta28be6d2016-06-16 18:09:35 -0700548 # This is used to hold striped
549 with osutils.TempDir(prefix='upload_symbols.') as tempdir:
550 symbols = FindSymbolFiles(tempdir, sym_paths)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500551
Don Garrett1bc1e102016-07-06 17:06:10 -0700552 # Sort all of our symbols so the largest ones (probably the most important)
553 # are processed first.
554 symbols = list(symbols)
555 symbols.sort(key=lambda s: s.FileSize(), reverse=True)
556
Don Garretta28be6d2016-06-16 18:09:35 -0700557 if upload_limit is not None:
558 # Restrict symbols processed to the limit.
559 symbols = itertools.islice(symbols, None, upload_limit)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400560
Don Garretta28be6d2016-06-16 18:09:35 -0700561 # Strip CFI, if needed.
562 symbols = (AdjustSymbolFileSize(s, tempdir, strip_cfi) for s in symbols)
Mike Frysinger8ec8c502014-02-10 00:19:13 -0500563
Mike Nichols137e82d2019-05-15 18:40:34 -0600564 # Find duplicates via batch API
565 symbols = FindDuplicates(symbols, upload_url, api_key, timeout)
566
Don Garretta28be6d2016-06-16 18:09:35 -0700567 # Perform uploads
Mike Nichols90f7c152019-04-09 15:14:08 -0600568 symbols = PerformSymbolsFileUpload(symbols, upload_url, api_key)
Mike Frysingerd42e5f02014-03-14 11:19:37 -0400569
Don Garretta28be6d2016-06-16 18:09:35 -0700570 # Log the final results, and consume the symbols generator fully.
Don Garrettdeb2e032016-07-06 16:44:14 -0700571 failures = ReportResults(symbols, failed_list)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500572
Don Garrettdeb2e032016-07-06 16:44:14 -0700573 return failures
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400574
575
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400576def main(argv):
577 parser = commandline.ArgumentParser(description=__doc__)
578
Don Garretta28be6d2016-06-16 18:09:35 -0700579 # TODO: Make sym_paths, breakpad_root, and root exclusive.
580
Mike Frysingerd41938e2014-02-10 06:37:55 -0500581 parser.add_argument('sym_paths', type='path_or_uri', nargs='*', default=None,
582 help='symbol file or directory or URL or tarball')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400583 parser.add_argument('--board', default=None,
Don Garrett747cc4b2015-10-07 14:48:48 -0700584 help='Used to find default breakpad_root.')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400585 parser.add_argument('--breakpad_root', type='path', default=None,
Mike Frysingerc5597f22014-11-27 15:39:15 -0500586 help='full path to the breakpad symbol directory')
587 parser.add_argument('--root', type='path', default=None,
588 help='full path to the chroot dir')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400589 parser.add_argument('--official_build', action='store_true', default=False,
590 help='point to official symbol server')
Mike Frysinger38647542014-09-12 18:15:39 -0700591 parser.add_argument('--server', type=str, default=None,
592 help='URI for custom symbol server')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400593 parser.add_argument('--regenerate', action='store_true', default=False,
594 help='regenerate all symbols')
Don Garretta28be6d2016-06-16 18:09:35 -0700595 parser.add_argument('--upload-limit', type=int,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400596 help='only upload # number of symbols')
597 parser.add_argument('--strip_cfi', type=int,
Don Garretta28be6d2016-06-16 18:09:35 -0700598 default=DEFAULT_FILE_LIMIT,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400599 help='strip CFI data for files above this size')
Mike Frysinger7f9be142014-01-15 02:16:42 -0500600 parser.add_argument('--failed-list', type='path',
601 help='where to save a list of failed symbols')
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500602 parser.add_argument('--dedupe', action='store_true', default=False,
603 help='use the swarming service to avoid re-uploading')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400604 parser.add_argument('--yes', action='store_true', default=False,
605 help='answer yes to all prompts')
Don Garrett747cc4b2015-10-07 14:48:48 -0700606 parser.add_argument('--product_name', type=str, default='ChromeOS',
607 help='Produce Name for breakpad stats.')
Mike Nichols90f7c152019-04-09 15:14:08 -0600608 parser.add_argument('--api_key', type=str, default=None,
609 help='full path to the API key file')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400610
611 opts = parser.parse_args(argv)
Mike Frysinger90e49ca2014-01-14 14:42:07 -0500612 opts.Freeze()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400613
Don Garretta28be6d2016-06-16 18:09:35 -0700614 # Figure out the symbol files/directories to upload.
615 if opts.sym_paths:
616 sym_paths = opts.sym_paths
617 elif opts.breakpad_root:
618 sym_paths = [opts.breakpad_root]
619 elif opts.root:
620 if not opts.board:
Mike Frysinger8390dec2017-08-11 15:11:38 -0400621 cros_build_lib.Die('--board must be set if --root is used.')
Don Garretta28be6d2016-06-16 18:09:35 -0700622 breakpad_dir = cros_generate_breakpad_symbols.FindBreakpadDir(opts.board)
623 sym_paths = [os.path.join(opts.root, breakpad_dir.lstrip('/'))]
624 else:
Mike Frysinger8390dec2017-08-11 15:11:38 -0400625 cros_build_lib.Die('--sym_paths, --breakpad_root, or --root must be set.')
Don Garretta28be6d2016-06-16 18:09:35 -0700626
Don Garrett747cc4b2015-10-07 14:48:48 -0700627 if opts.sym_paths or opts.breakpad_root:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400628 if opts.regenerate:
Don Garrett747cc4b2015-10-07 14:48:48 -0700629 cros_build_lib.Die('--regenerate may not be used with specific files, '
630 'or breakpad_root')
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400631 else:
632 if opts.board is None:
633 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400634
Don Garretta28be6d2016-06-16 18:09:35 -0700635 # Figure out which crash server to upload too.
636 upload_url = opts.server
637 if not upload_url:
638 if opts.official_build:
639 upload_url = OFFICIAL_UPLOAD_URL
640 else:
641 logging.warning('unofficial builds upload to the staging server')
642 upload_url = STAGING_UPLOAD_URL
643
Mike Nichols90f7c152019-04-09 15:14:08 -0600644 # Set up the API key needed to authenticate to Crash server.
645 # Allow for a local key file for testing purposes.
646 if opts.api_key:
647 api_key_file = opts.api_key
648 else:
649 api_key_file = constants.CRASH_API_KEY
650
651 api_key = osutils.ReadFile(api_key_file)
652
Don Garretta28be6d2016-06-16 18:09:35 -0700653 # Confirm we really want the long upload.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400654 if not opts.yes:
Mike Frysingerc5de9602014-02-09 02:42:36 -0500655 prolog = '\n'.join(textwrap.wrap(textwrap.dedent("""
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400656 Uploading symbols for an entire Chromium OS build is really only
657 necessary for release builds and in a few cases for developers
658 to debug problems. It will take considerable time to run. For
659 developer debugging purposes, consider instead passing specific
660 files to upload.
Mike Frysingerc5de9602014-02-09 02:42:36 -0500661 """), 80)).strip()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400662 if not cros_build_lib.BooleanPrompt(
663 prompt='Are you sure you want to upload all build symbols',
Mike Frysingerc5de9602014-02-09 02:42:36 -0500664 default=False, prolog=prolog):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400665 cros_build_lib.Die('better safe than sorry')
666
667 ret = 0
Don Garretta28be6d2016-06-16 18:09:35 -0700668
669 # Regenerate symbols from binaries.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400670 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400671 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
672 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400673
Don Garretta28be6d2016-06-16 18:09:35 -0700674 # Do the upload.
675 ret += UploadSymbols(
676 sym_paths=sym_paths,
677 upload_url=upload_url,
Don Garretta28be6d2016-06-16 18:09:35 -0700678 failed_list=opts.failed_list,
679 upload_limit=opts.upload_limit,
Mike Nichols90f7c152019-04-09 15:14:08 -0600680 strip_cfi=opts.strip_cfi,
681 api_key=api_key)
Don Garretta28be6d2016-06-16 18:09:35 -0700682
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400683 if ret:
Ralph Nathan59900422015-03-24 10:41:17 -0700684 logging.error('encountered %i problem(s)', ret)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400685 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
686 # return 0 in case we are a multiple of the mask.
Don Garretta28be6d2016-06-16 18:09:35 -0700687 return 1