blob: d5b52e0196ddd77136c5dd308d1e76798439d490 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2013 The ChromiumOS Authors
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -04002# 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.
Alex Klein1699fab2022-09-08 08:46:06 -060042sys.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.
Alex Klein1699fab2022-09-08 08:46:06 -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.
Alex Klein1699fab2022-09-08 08:46:06 -060097UPLOAD_STATS = "UPLOAD"
Don Garrette1f47e92016-10-13 16:04:56 -070098
Don Garretta28be6d2016-06-16 18:09:35 -070099
Don Garretta28be6d2016-06-16 18:09:35 -0700100def BatchGenerator(iterator, batch_size):
Alex Klein1699fab2022-09-08 08:46:06 -0600101 """Given an iterator, break into lists of size batch_size.
Fang Dengba680462015-08-16 20:34:11 -0700102
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000103 The result is a generator, that will only read in as many inputs as needed
104 for the current batch. The final result can be smaller than batch_size.
Alex Klein1699fab2022-09-08 08:46:06 -0600105 """
106 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
Alex Klein1699fab2022-09-08 08:46:06 -0600113 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):
Alex Klein1699fab2022-09-08 08:46:06 -0600119 """Guess if this is a tarball based on the filename."""
120 parts = path.split(".")
121 if len(parts) <= 1:
122 return False
Mike Frysingerd41938e2014-02-10 06:37:55 -0500123
Alex Klein1699fab2022-09-08 08:46:06 -0600124 if parts[-1] == "tar":
125 return True
Mike Frysingerd41938e2014-02-10 06:37:55 -0500126
Alex Klein1699fab2022-09-08 08:46:06 -0600127 if parts[-2] == "tar":
128 return parts[-1] in ("bz2", "gz", "xz")
Mike Frysingerd41938e2014-02-10 06:37:55 -0500129
Alex Klein1699fab2022-09-08 08:46:06 -0600130 return parts[-1] in ("tbz2", "tbz", "tgz", "txz")
Mike Frysingerd41938e2014-02-10 06:37:55 -0500131
132
Don Garretta28be6d2016-06-16 18:09:35 -0700133class SymbolFile(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600134 """This class represents the state of a symbol file during processing.
Don Garretta28be6d2016-06-16 18:09:35 -0700135
Alex Klein1699fab2022-09-08 08:46:06 -0600136 Attributes:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000137 display_path: Name of symbol file that should be consistent between
138 builds.
139 file_name: Transient path of the symbol file.
140 header: ReadSymsHeader output. Dict with assorted meta-data.
141 status: INITIAL, DUPLICATE, or UPLOADED based on status of processing.
142 dedupe_item: None or instance of DedupeItem for this symbol file.
143 dedupe_push_state: Opaque value to return to dedupe code for file.
144 display_name: Read only friendly (short) file name for logging.
145 file_size: Read only size of the symbol file.
Don Garretta28be6d2016-06-16 18:09:35 -0700146 """
Don Garretta28be6d2016-06-16 18:09:35 -0700147
Alex Klein1699fab2022-09-08 08:46:06 -0600148 INITIAL = "initial"
149 DUPLICATE = "duplicate"
150 UPLOADED = "uploaded"
151 ERROR = "error"
Don Garretta28be6d2016-06-16 18:09:35 -0700152
Alex Klein1699fab2022-09-08 08:46:06 -0600153 def __init__(self, display_path, file_name):
154 """An instance of this class represents a symbol file over time.
155
156 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000157 display_path: A unique/persistent between builds name to present to
158 the crash server. It is the file name, relative to where it came
159 from (tarball, breakpad dir, etc).
160 file_name: A the current location of the symbol file.
Alex Klein1699fab2022-09-08 08:46:06 -0600161 """
162 self.display_path = display_path
163 self.file_name = file_name
Ian Barkley-Yeungfdaaf2e2023-03-02 17:30:39 -0800164 self.header = cros_generate_breakpad_symbols.ReadSymsHeader(
165 file_name, file_name
166 )
Alex Klein1699fab2022-09-08 08:46:06 -0600167 self.status = SymbolFile.INITIAL
168
169 @property
170 def display_name(self):
171 return os.path.basename(self.display_path)
172
173 def FileSize(self):
174 return os.path.getsize(self.file_name)
Don Garretta28be6d2016-06-16 18:09:35 -0700175
176
Don Garretta28be6d2016-06-16 18:09:35 -0700177def FindSymbolFiles(tempdir, paths):
Alex Klein1699fab2022-09-08 08:46:06 -0600178 """Locate symbol files in |paths|
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500179
Alex Klein1699fab2022-09-08 08:46:06 -0600180 This returns SymbolFile objects that contain file references which are valid
181 after this exits. Those files may exist externally, or be created in the
182 tempdir (say, when expanding tarballs). The caller must not consider
183 SymbolFile's valid after tempdir is cleaned up.
Don Garretta28be6d2016-06-16 18:09:35 -0700184
Alex Klein1699fab2022-09-08 08:46:06 -0600185 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000186 tempdir: Path to use for temporary files.
187 paths: A list of input paths to walk. Files are returned w/out any
188 checks. Dirs are searched for files that end in ".sym". Urls are
189 fetched and then processed. Tarballs are unpacked and walked.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500190
Alex Klein1699fab2022-09-08 08:46:06 -0600191 Yields:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000192 A SymbolFile for every symbol file found in paths.
Alex Klein1699fab2022-09-08 08:46:06 -0600193 """
194 cache_dir = path_util.GetCacheDir()
195 common_path = os.path.join(cache_dir, constants.COMMON_CACHE)
196 tar_cache = cache.TarballCache(common_path)
Mike Frysingere847efd2015-01-08 03:57:24 -0500197
Alex Klein1699fab2022-09-08 08:46:06 -0600198 for p in paths:
199 o = urllib.parse.urlparse(p)
200 if o.scheme:
201 # Support globs of filenames.
202 ctx = gs.GSContext()
203 for gspath in ctx.LS(p):
204 logging.info("processing files inside %s", gspath)
205 o = urllib.parse.urlparse(gspath)
206 key = ("%s%s" % (o.netloc, o.path)).split("/")
207 # The common cache will not be LRU, removing the need to hold a read
208 # lock on the cached gsutil.
209 ref = tar_cache.Lookup(key)
210 try:
211 ref.SetDefault(gspath)
212 except cros_build_lib.RunCommandError as e:
213 logging.warning("ignoring %s\n%s", gspath, e)
214 continue
215 for sym in FindSymbolFiles(tempdir, [ref.path]):
216 yield sym
Mike Frysingerd41938e2014-02-10 06:37:55 -0500217
Alex Klein1699fab2022-09-08 08:46:06 -0600218 elif os.path.isdir(p):
219 for root, _, files in os.walk(p):
220 for f in files:
221 if f.endswith(".sym"):
222 # If p is '/tmp/foo' and filename is '/tmp/foo/bar/bar.sym',
223 # display_path = 'bar/bar.sym'
224 filename = os.path.join(root, f)
225 yield SymbolFile(
226 display_path=filename[len(p) :].lstrip("/"),
227 file_name=filename,
228 )
Mike Frysingerd41938e2014-02-10 06:37:55 -0500229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 elif IsTarball(p):
231 logging.info("processing files inside %s", p)
232 tardir = tempfile.mkdtemp(dir=tempdir)
233 cache.Untar(os.path.realpath(p), tardir)
234 for sym in FindSymbolFiles(tardir, [tardir]):
235 yield sym
Mike Frysingerd41938e2014-02-10 06:37:55 -0500236
Alex Klein1699fab2022-09-08 08:46:06 -0600237 else:
238 yield SymbolFile(display_path=p, file_name=p)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500239
240
Don Garretta28be6d2016-06-16 18:09:35 -0700241def AdjustSymbolFileSize(symbol, tempdir, file_limit):
Alex Klein1699fab2022-09-08 08:46:06 -0600242 """Examine symbols files for size problems, and reduce if needed.
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500243
Alex Klein1699fab2022-09-08 08:46:06 -0600244 If the symbols size is too big, strip out the call frame info. The CFI
245 is unnecessary for 32bit x86 targets where the frame pointer is used (as
246 all of ours have) and it accounts for over half the size of the symbols
247 uploaded.
Don Garretta28be6d2016-06-16 18:09:35 -0700248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 Stripped files will be created inside tempdir, and will be the callers
250 responsibility to clean up.
Don Garretta28be6d2016-06-16 18:09:35 -0700251
Alex Klein1699fab2022-09-08 08:46:06 -0600252 We also warn, if a symbols file is still too large after stripping.
Mike Frysinger5e6dd712014-03-07 22:21:17 -0500253
Alex Klein1699fab2022-09-08 08:46:06 -0600254 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000255 symbol: SymbolFile instance to be examined and modified as needed..
256 tempdir: A temporary directory we can create files in that the caller
257 will clean up.
258 file_limit: We only strip files which are larger than this limit.
Don Garretta28be6d2016-06-16 18:09:35 -0700259
Alex Klein1699fab2022-09-08 08:46:06 -0600260 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000261 SymbolFile instance (original or modified as needed)
Alex Klein1699fab2022-09-08 08:46:06 -0600262 """
263 file_size = symbol.FileSize()
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500264
Alex Klein1699fab2022-09-08 08:46:06 -0600265 if file_limit and symbol.FileSize() > file_limit:
266 with cros_build_lib.UnbufferedNamedTemporaryFile(
267 prefix="upload_symbols", dir=tempdir, delete=False
268 ) as temp_sym_file:
Alex Klein1699fab2022-09-08 08:46:06 -0600269 with open(symbol.file_name, "rb") as fp:
270 temp_sym_file.writelines(
271 x for x in fp.readlines() if not x.startswith(b"STACK CFI")
272 )
273
274 original_file_size = file_size
275 symbol.file_name = temp_sym_file.name
276 file_size = symbol.FileSize()
277
278 logging.warning(
279 "stripped CFI for %s reducing size %s > %s",
280 symbol.display_name,
281 original_file_size,
282 file_size,
283 )
284
285 # Hopefully the crash server will let it through. But it probably won't.
286 # Not sure what the best answer is in this case.
287 if file_size >= CRASH_SERVER_FILE_LIMIT:
288 cbuildbot_alerts.PrintBuildbotStepWarnings()
289 logging.warning(
290 "upload file %s is awfully large, risking rejection by "
291 "the symbol server (%s > %s)",
292 symbol.display_path,
293 file_size,
294 CRASH_SERVER_FILE_LIMIT,
Mike Frysingerac75f602019-11-14 23:18:51 -0500295 )
Don Garretta28be6d2016-06-16 18:09:35 -0700296
Alex Klein1699fab2022-09-08 08:46:06 -0600297 return symbol
Don Garretta28be6d2016-06-16 18:09:35 -0700298
Don Garretta28be6d2016-06-16 18:09:35 -0700299
300def GetUploadTimeout(symbol):
Alex Klein1699fab2022-09-08 08:46:06 -0600301 """How long to wait for a specific file to upload to the crash server.
Don Garretta28be6d2016-06-16 18:09:35 -0700302
Alex Klein1699fab2022-09-08 08:46:06 -0600303 This is a function largely to make unittesting easier.
Don Garretta28be6d2016-06-16 18:09:35 -0700304
Alex Klein1699fab2022-09-08 08:46:06 -0600305 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000306 symbol: A SymbolFile instance.
Don Garretta28be6d2016-06-16 18:09:35 -0700307
Alex Klein1699fab2022-09-08 08:46:06 -0600308 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000309 Timeout length (in seconds)
Alex Klein1699fab2022-09-08 08:46:06 -0600310 """
311 # Scale the timeout based on the filesize.
312 return max(symbol.FileSize() // UPLOAD_MIN_RATE, UPLOAD_MIN_TIMEOUT)
Don Garretta28be6d2016-06-16 18:09:35 -0700313
314
Mike Nichols90f7c152019-04-09 15:14:08 -0600315def ExecRequest(operator, url, timeout, api_key, **kwargs):
Alex Klein1699fab2022-09-08 08:46:06 -0600316 """Makes a web request with default timeout, returning the json result.
Don Garretta28be6d2016-06-16 18:09:35 -0700317
Alex Klein1699fab2022-09-08 08:46:06 -0600318 This method will raise a requests.exceptions.HTTPError if the status
319 code is not 4XX, 5XX
Mike Nichols90f7c152019-04-09 15:14:08 -0600320
Alex Klein1699fab2022-09-08 08:46:06 -0600321 Note: If you are using verbose logging it is entirely possible that the
322 subsystem will write your api key to the logs!
Mike Nichols90f7c152019-04-09 15:14:08 -0600323
Alex Klein1699fab2022-09-08 08:46:06 -0600324 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000325 operator: HTTP method.
326 url: Endpoint URL.
327 timeout: HTTP timeout for request.
328 api_key: Authentication key.
Mike Nichols90f7c152019-04-09 15:14:08 -0600329
Alex Klein1699fab2022-09-08 08:46:06 -0600330 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000331 HTTP response content
Alex Klein1699fab2022-09-08 08:46:06 -0600332 """
333 resp = requests.request(
334 operator,
335 url,
336 params={"key": api_key},
337 headers={"User-agent": "chromite.upload_symbols"},
338 timeout=timeout,
339 **kwargs,
340 )
341 # Make sure we don't leak secret keys by accident.
342 if resp.status_code > 399:
343 resp.url = resp.url.replace(urllib.parse.quote(api_key), "XX-HIDDEN-XX")
344 logging.warning(
345 'Url: %s, Status: %s, response: "%s", in: %s',
346 resp.url,
347 resp.status_code,
348 resp.text,
349 resp.elapsed,
350 )
351 elif resp.content:
352 return resp.json()
Mike Nicholscf5b7a92019-04-25 12:01:28 -0600353
Alex Klein1699fab2022-09-08 08:46:06 -0600354 return {}
Mike Nichols90f7c152019-04-09 15:14:08 -0600355
356
Mike Nichols137e82d2019-05-15 18:40:34 -0600357def FindDuplicates(symbols, status_url, api_key, timeout=DEDUPE_TIMEOUT):
Alex Klein1699fab2022-09-08 08:46:06 -0600358 """Check whether the symbol files have already been uploaded.
Mike Nichols649e6a82019-04-23 11:44:48 -0600359
Alex Klein1699fab2022-09-08 08:46:06 -0600360 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000361 symbols: A iterable of SymbolFiles to be uploaded
362 status_url: The crash URL to validate the file existence.
363 api_key: Authentication key.
364 timeout: HTTP timeout for request.
Mike Nichols649e6a82019-04-23 11:44:48 -0600365
Alex Klein1699fab2022-09-08 08:46:06 -0600366 Yields:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000367 All SymbolFiles from symbols, but duplicates have status updated to
368 DUPLICATE.
Alex Klein1699fab2022-09-08 08:46:06 -0600369 """
370 for batch in BatchGenerator(symbols, DEDUPE_LIMIT):
371 items = []
372 result = {}
373 for x in batch:
374 items.append(
375 {
376 "debug_file": x.header.name,
377 "debug_id": x.header.id.replace("-", ""),
378 }
379 )
380 symbol_data = {"symbol_ids": items}
381 try:
382 result = ExecRequest(
383 "post",
384 "%s/symbols:checkStatuses" % status_url,
385 timeout,
386 api_key=api_key,
387 data=json.dumps(symbol_data),
388 )
389 except requests.exceptions.RequestException as e:
390 logging.warning("could not identify duplicates: HTTP error: %s", e)
391 for b in batch:
392 b.status = SymbolFile.INITIAL
393 set_match = {
394 "debugId": b.header.id.replace("-", ""),
395 "debugFile": b.header.name,
396 }
397 for cs_result in result.get("pairs", []):
398 if set_match == cs_result.get("symbolId"):
399 if cs_result.get("status") == "FOUND":
400 logging.debug("Found duplicate: %s", b.display_name)
401 b.status = SymbolFile.DUPLICATE
402 break
403 yield b
Mike Nichols137e82d2019-05-15 18:40:34 -0600404
Mike Nichols649e6a82019-04-23 11:44:48 -0600405
Mike Nichols90f7c152019-04-09 15:14:08 -0600406def UploadSymbolFile(upload_url, symbol, api_key):
Alex Klein1699fab2022-09-08 08:46:06 -0600407 """Upload a symbol file to the crash server, returning the status result.
Don Garretta28be6d2016-06-16 18:09:35 -0700408
Alex Klein1699fab2022-09-08 08:46:06 -0600409 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000410 upload_url: The crash URL to POST the |sym_file| to
411 symbol: A SymbolFile instance.
412 api_key: Authentication key
Alex Klein1699fab2022-09-08 08:46:06 -0600413 """
414 timeout = GetUploadTimeout(symbol)
415 logging.debug("Executing post to uploads:create: %s", symbol.display_name)
416 upload = ExecRequest(
417 "post", "%s/uploads:create" % upload_url, timeout, api_key
418 )
Mike Nichols649e6a82019-04-23 11:44:48 -0600419
Alex Klein1699fab2022-09-08 08:46:06 -0600420 if upload and "uploadUrl" in upload.keys():
421 symbol_data = {
422 "symbol_id": {
423 "debug_file": symbol.header.name,
424 "debug_id": symbol.header.id.replace("-", ""),
425 }
426 }
Mike Frysinger31fdddd2023-02-24 15:50:55 -0500427 with open(symbol.file_name, "r", encoding="utf-8") as fp:
Alex Klein1699fab2022-09-08 08:46:06 -0600428 logging.debug("Executing put to uploadUrl: %s", symbol.display_name)
429 ExecRequest(
430 "put",
431 upload["uploadUrl"],
432 timeout,
433 api_key=api_key,
434 data=fp.read(),
435 )
436 logging.debug(
437 "Executing post to uploads/complete: %s", symbol.display_name
438 )
439 ExecRequest(
440 "post",
441 "%s/uploads/%s:complete" % (upload_url, upload["uploadKey"]),
442 timeout,
443 api_key=api_key,
444 # TODO(mikenichols): Validate product_name once it is added
445 # to the proto; currently unsupported.
446 data=json.dumps(symbol_data),
447 )
448 else:
449 raise requests.exceptions.HTTPError
Don Garretta28be6d2016-06-16 18:09:35 -0700450
451
Mike Nichols90f7c152019-04-09 15:14:08 -0600452def PerformSymbolsFileUpload(symbols, upload_url, api_key):
Alex Klein1699fab2022-09-08 08:46:06 -0600453 """Upload the symbols to the crash server
Don Garretta28be6d2016-06-16 18:09:35 -0700454
Alex Klein1699fab2022-09-08 08:46:06 -0600455 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000456 symbols: An iterable of SymbolFiles to be uploaded.
457 upload_url: URL of crash server to upload too.
458 api_key: Authentication key.
459 failures: Tracker for total upload failures.
Don Garretta28be6d2016-06-16 18:09:35 -0700460
Alex Klein1699fab2022-09-08 08:46:06 -0600461 Yields:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000462 Each symbol from symbols, perhaps modified.
Alex Klein1699fab2022-09-08 08:46:06 -0600463 """
464 failures = 0
465 # Failures are figured per request, therefore each HTTP failure
466 # counts against the max. This means that the process will exit
467 # at the point when HTTP errors hit the defined limit.
468 for s in symbols:
469 if failures < MAX_TOTAL_ERRORS_FOR_RETRY and s.status in (
470 SymbolFile.INITIAL,
471 SymbolFile.ERROR,
472 ):
473 # Keeps us from DoS-ing the symbol server.
474 time.sleep(SLEEP_DELAY)
475 logging.info("Uploading symbol_file: %s", s.display_path)
476 try:
477 # This command retries the upload multiple times with growing delays. We
478 # only consider the upload a failure if these retries fail.
479 def ShouldRetryUpload(exception):
480 if isinstance(
481 exception,
482 (
483 requests.exceptions.RequestException,
484 IOError,
485 http.client.HTTPException,
486 socket.error,
487 ),
488 ):
489 logging.info("Request failed, retrying: %s", exception)
490 return True
491 return False
Don Garrette1f47e92016-10-13 16:04:56 -0700492
Alex Klein1699fab2022-09-08 08:46:06 -0600493 with timer.Timer() as t:
494 retry_stats.RetryWithStats(
495 UPLOAD_STATS,
496 ShouldRetryUpload,
497 MAX_RETRIES,
498 UploadSymbolFile,
499 upload_url,
500 s,
501 api_key,
502 sleep=INITIAL_RETRY_DELAY,
503 log_all_retries=True,
504 )
505 if s.status != SymbolFile.DUPLICATE:
506 logging.info(
507 "upload of %s with size %10i bytes took %s",
508 s.display_name,
509 s.FileSize(),
510 t,
511 )
512 s.status = SymbolFile.UPLOADED
513 except requests.exceptions.RequestException as e:
514 logging.warning(
515 "could not upload: %s: HTTP error: %s", s.display_name, e
516 )
517 s.status = SymbolFile.ERROR
518 failures += 1
519 except (http.client.HTTPException, OSError) as e:
520 logging.warning(
521 "could not upload: %s: %s %s",
522 s.display_name,
523 type(e).__name__,
524 e,
525 )
526 s.status = SymbolFile.ERROR
527 failures += 1
Don Garrettdeb2e032016-07-06 16:44:14 -0700528
Alex Klein1699fab2022-09-08 08:46:06 -0600529 # We pass the symbol along, on both success and failure.
530 yield s
Don Garretta28be6d2016-06-16 18:09:35 -0700531
532
Don Garrettdeb2e032016-07-06 16:44:14 -0700533def ReportResults(symbols, failed_list):
Alex Klein1699fab2022-09-08 08:46:06 -0600534 """Log a summary of the symbol uploading.
Don Garretta28be6d2016-06-16 18:09:35 -0700535
Alex Klein1699fab2022-09-08 08:46:06 -0600536 This has the side effect of fully consuming the symbols iterator.
Don Garretta28be6d2016-06-16 18:09:35 -0700537
Alex Klein1699fab2022-09-08 08:46:06 -0600538 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000539 symbols: An iterator of SymbolFiles to be uploaded.
540 failed_list: A filename at which to write out a list of our failed
541 uploads.
Don Garrettdeb2e032016-07-06 16:44:14 -0700542
Alex Klein1699fab2022-09-08 08:46:06 -0600543 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000544 The number of symbols not uploaded.
Alex Klein1699fab2022-09-08 08:46:06 -0600545 """
546 upload_failures = []
547 result_counts = {
548 SymbolFile.INITIAL: 0,
549 SymbolFile.UPLOADED: 0,
550 SymbolFile.DUPLICATE: 0,
551 SymbolFile.ERROR: 0,
552 }
Don Garretta28be6d2016-06-16 18:09:35 -0700553
Alex Klein1699fab2022-09-08 08:46:06 -0600554 for s in symbols:
555 result_counts[s.status] += 1
556 if s.status in [SymbolFile.INITIAL, SymbolFile.ERROR]:
557 upload_failures.append(s)
Don Garretta28be6d2016-06-16 18:09:35 -0700558
Alex Klein1699fab2022-09-08 08:46:06 -0600559 # Report retry numbers.
560 _, _, retries = retry_stats.CategoryStats(UPLOAD_STATS)
561 if retries:
562 logging.warning("%d upload retries performed.", retries)
Don Garrette1f47e92016-10-13 16:04:56 -0700563
Alex Klein1699fab2022-09-08 08:46:06 -0600564 logging.info(
565 "Uploaded %(uploaded)d, Skipped %(duplicate)d duplicates.",
566 result_counts,
567 )
Don Garretta28be6d2016-06-16 18:09:35 -0700568
Alex Klein1699fab2022-09-08 08:46:06 -0600569 if result_counts[SymbolFile.ERROR]:
570 cbuildbot_alerts.PrintBuildbotStepWarnings()
571 logging.warning(
572 "%d non-recoverable upload errors", result_counts[SymbolFile.ERROR]
573 )
Chris Ching91908032016-09-27 16:55:33 -0600574
Alex Klein1699fab2022-09-08 08:46:06 -0600575 if result_counts[SymbolFile.INITIAL]:
576 cbuildbot_alerts.PrintBuildbotStepWarnings()
577 logging.warning(
578 "%d upload(s) were skipped because of excessive errors",
579 result_counts[SymbolFile.INITIAL],
580 )
Don Garretta28be6d2016-06-16 18:09:35 -0700581
Alex Klein1699fab2022-09-08 08:46:06 -0600582 if failed_list is not None:
Mike Frysinger31fdddd2023-02-24 15:50:55 -0500583 with open(failed_list, "w", encoding="utf-8") as fl:
Alex Klein1699fab2022-09-08 08:46:06 -0600584 for s in upload_failures:
585 fl.write("%s\n" % s.display_path)
Don Garretta28be6d2016-06-16 18:09:35 -0700586
Alex Klein1699fab2022-09-08 08:46:06 -0600587 return result_counts[SymbolFile.INITIAL] + result_counts[SymbolFile.ERROR]
Don Garrettdeb2e032016-07-06 16:44:14 -0700588
Don Garretta28be6d2016-06-16 18:09:35 -0700589
Alex Klein1699fab2022-09-08 08:46:06 -0600590def UploadSymbols(
591 sym_paths,
592 upload_url,
593 failed_list=None,
594 upload_limit=None,
595 strip_cfi=None,
596 timeout=None,
597 api_key=None,
598):
599 """Upload all the generated symbols for |board| to the crash server
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400600
Alex Klein1699fab2022-09-08 08:46:06 -0600601 Args:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000602 sym_paths: Specific symbol files (or dirs of sym files) to upload,
603 otherwise search |breakpad_dir|
604 upload_url: URL of crash server to upload too.
605 failed_list: A filename at which to write out a list of our failed
606 uploads.
607 upload_limit: Integer listing how many files to upload. None for no
608 limit.
609 strip_cfi: File size at which we strip out CFI data. None for no limit.
610 timeout: HTTP timeout for request.
611 api_key: A string based authentication key
Mike Frysinger1a736a82013-12-12 01:50:59 -0500612
Alex Klein1699fab2022-09-08 08:46:06 -0600613 Returns:
Trent Aptedc20bb6d2023-05-10 15:00:03 +1000614 The number of errors that were encountered.
Alex Klein1699fab2022-09-08 08:46:06 -0600615 """
616 retry_stats.SetupStats()
Don Garrette1f47e92016-10-13 16:04:56 -0700617
Alex Klein1699fab2022-09-08 08:46:06 -0600618 # Note: This method looks like each step of processing is performed
619 # sequentially for all SymbolFiles, but instead each step is a generator that
620 # produces the next iteration only when it's read. This means that (except for
621 # some batching) each SymbolFile goes through all of these steps before the
622 # next one is processed at all.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400623
Alex Klein1699fab2022-09-08 08:46:06 -0600624 # This is used to hold striped
625 with osutils.TempDir(prefix="upload_symbols.") as tempdir:
626 symbols = FindSymbolFiles(tempdir, sym_paths)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500627
Alex Klein1699fab2022-09-08 08:46:06 -0600628 # Sort all of our symbols so the largest ones (probably the most important)
629 # are processed first.
630 symbols = list(symbols)
631 symbols.sort(key=lambda s: s.FileSize(), reverse=True)
Don Garrett1bc1e102016-07-06 17:06:10 -0700632
Alex Klein1699fab2022-09-08 08:46:06 -0600633 if upload_limit is not None:
634 # Restrict symbols processed to the limit.
635 symbols = itertools.islice(symbols, None, upload_limit)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400636
Alex Klein1699fab2022-09-08 08:46:06 -0600637 # Strip CFI, if needed.
638 symbols = (AdjustSymbolFileSize(s, tempdir, strip_cfi) for s in symbols)
Mike Frysinger8ec8c502014-02-10 00:19:13 -0500639
Alex Klein1699fab2022-09-08 08:46:06 -0600640 # Find duplicates via batch API
641 symbols = FindDuplicates(symbols, upload_url, api_key, timeout)
Mike Nichols137e82d2019-05-15 18:40:34 -0600642
Alex Klein1699fab2022-09-08 08:46:06 -0600643 # Perform uploads
644 symbols = PerformSymbolsFileUpload(symbols, upload_url, api_key)
Mike Frysingerd42e5f02014-03-14 11:19:37 -0400645
Alex Klein1699fab2022-09-08 08:46:06 -0600646 # Log the final results, and consume the symbols generator fully.
647 failures = ReportResults(symbols, failed_list)
Mike Frysinger0c0efa22014-02-09 23:32:23 -0500648
Alex Klein1699fab2022-09-08 08:46:06 -0600649 return failures
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400650
651
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400652def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600653 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400654
Alex Klein1699fab2022-09-08 08:46:06 -0600655 # TODO: Make sym_paths, breakpad_root, and root exclusive.
Don Garretta28be6d2016-06-16 18:09:35 -0700656
Alex Klein1699fab2022-09-08 08:46:06 -0600657 parser.add_argument(
658 "sym_paths",
659 type="path_or_uri",
660 nargs="*",
661 default=None,
662 help="symbol file or directory or URL or tarball",
663 )
664 parser.add_argument(
665 "--board", default=None, help="Used to find default breakpad_root."
666 )
667 parser.add_argument(
668 "--breakpad_root",
669 type="path",
670 default=None,
671 help="full path to the breakpad symbol directory",
672 )
673 parser.add_argument(
674 "--root", type="path", default=None, help="full path to the chroot dir"
675 )
676 parser.add_argument(
677 "--official_build",
678 action="store_true",
679 default=False,
680 help="point to official symbol server",
681 )
682 parser.add_argument(
683 "--server", type=str, default=None, help="URI for custom symbol server"
684 )
685 parser.add_argument(
686 "--regenerate",
687 action="store_true",
688 default=False,
689 help="regenerate all symbols",
690 )
691 parser.add_argument(
692 "--upload-limit", type=int, help="only upload # number of symbols"
693 )
694 parser.add_argument(
695 "--strip_cfi",
696 type=int,
697 default=DEFAULT_FILE_LIMIT,
698 help="strip CFI data for files above this size",
699 )
700 parser.add_argument(
701 "--failed-list",
702 type="path",
703 help="where to save a list of failed symbols",
704 )
705 parser.add_argument(
706 "--dedupe",
707 action="store_true",
708 default=False,
709 help="use the swarming service to avoid re-uploading",
710 )
711 parser.add_argument(
712 "--yes",
713 action="store_true",
714 default=False,
715 help="answer yes to all prompts",
716 )
717 parser.add_argument(
718 "--product_name",
719 type=str,
720 default="ChromeOS",
721 help="Produce Name for breakpad stats.",
722 )
723 parser.add_argument(
724 "--api_key",
725 type=str,
726 default=None,
727 help="full path to the API key file",
728 )
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400729
Alex Klein1699fab2022-09-08 08:46:06 -0600730 opts = parser.parse_args(argv)
731 opts.Freeze()
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400732
Alex Klein1699fab2022-09-08 08:46:06 -0600733 # Figure out the symbol files/directories to upload.
734 if opts.sym_paths:
735 sym_paths = opts.sym_paths
736 elif opts.breakpad_root:
737 sym_paths = [opts.breakpad_root]
738 elif opts.root:
739 if not opts.board:
740 cros_build_lib.Die("--board must be set if --root is used.")
741 breakpad_dir = cros_generate_breakpad_symbols.FindBreakpadDir(
742 opts.board
743 )
744 sym_paths = [os.path.join(opts.root, breakpad_dir.lstrip("/"))]
Don Garretta28be6d2016-06-16 18:09:35 -0700745 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600746 cros_build_lib.Die(
747 "--sym_paths, --breakpad_root, or --root must be set."
748 )
Don Garretta28be6d2016-06-16 18:09:35 -0700749
Alex Klein1699fab2022-09-08 08:46:06 -0600750 if opts.sym_paths or opts.breakpad_root:
751 if opts.regenerate:
752 cros_build_lib.Die(
753 "--regenerate may not be used with specific files, "
754 "or breakpad_root"
755 )
756 else:
757 if opts.board is None:
758 cros_build_lib.Die("--board is required")
Mike Nichols90f7c152019-04-09 15:14:08 -0600759
Alex Klein1699fab2022-09-08 08:46:06 -0600760 # Figure out which crash server to upload too.
761 upload_url = opts.server
762 if not upload_url:
763 if opts.official_build:
764 upload_url = OFFICIAL_UPLOAD_URL
765 else:
766 logging.warning("unofficial builds upload to the staging server")
767 upload_url = STAGING_UPLOAD_URL
Mike Nichols90f7c152019-04-09 15:14:08 -0600768
Alex Klein1699fab2022-09-08 08:46:06 -0600769 # Set up the API key needed to authenticate to Crash server.
770 # Allow for a local key file for testing purposes.
771 if opts.api_key:
772 api_key_file = opts.api_key
773 else:
774 api_key_file = constants.CRASH_API_KEY
775
776 api_key = osutils.ReadFile(api_key_file)
777
778 # Confirm we really want the long upload.
779 if not opts.yes:
780 prolog = "\n".join(
781 textwrap.wrap(
782 textwrap.dedent(
783 """
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400784 Uploading symbols for an entire Chromium OS build is really only
785 necessary for release builds and in a few cases for developers
786 to debug problems. It will take considerable time to run. For
787 developer debugging purposes, consider instead passing specific
788 files to upload.
Alex Klein1699fab2022-09-08 08:46:06 -0600789 """
790 ),
791 80,
792 )
793 ).strip()
794 if not cros_build_lib.BooleanPrompt(
795 prompt="Are you sure you want to upload all build symbols",
796 default=False,
797 prolog=prolog,
798 ):
799 cros_build_lib.Die("better safe than sorry")
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400800
Alex Klein1699fab2022-09-08 08:46:06 -0600801 ret = 0
Don Garretta28be6d2016-06-16 18:09:35 -0700802
Alex Klein1699fab2022-09-08 08:46:06 -0600803 # Regenerate symbols from binaries.
804 if opts.regenerate:
805 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
806 opts.board, breakpad_dir=opts.breakpad_root
807 )
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400808
Alex Klein1699fab2022-09-08 08:46:06 -0600809 # Do the upload.
810 ret += UploadSymbols(
811 sym_paths=sym_paths,
812 upload_url=upload_url,
813 failed_list=opts.failed_list,
814 upload_limit=opts.upload_limit,
815 strip_cfi=opts.strip_cfi,
816 api_key=api_key,
817 )
Don Garretta28be6d2016-06-16 18:09:35 -0700818
Alex Klein1699fab2022-09-08 08:46:06 -0600819 if ret:
820 logging.error("encountered %i problem(s)", ret)
821 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
822 # return 0 in case we are a multiple of the mask.
823 return 1