blob: ddaef729154db501a1d190bfef644a6a764ea718 [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
12import ctypes
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040013import multiprocessing
14import os
Mike Frysinger094a2172013-08-14 12:54:35 -040015import poster
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040016import random
17import textwrap
18import tempfile
19import time
Mike Frysinger094a2172013-08-14 12:54:35 -040020import urllib2
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040021
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040022from chromite.lib import commandline
23from chromite.lib import cros_build_lib
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040024from chromite.lib import parallel
Mike Frysinger69cb41d2013-08-11 20:08:19 -040025from chromite.scripts import cros_generate_breakpad_symbols
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040026
27
28# URLs used for uploading symbols.
29OFFICIAL_UPLOAD_URL = 'http://clients2.google.com/cr/symbol'
30STAGING_UPLOAD_URL = 'http://clients2.google.com/cr/staging_symbol'
31
32
33# The crash server rejects files that are this big.
34CRASH_SERVER_FILE_LIMIT = 350 * 1024 * 1024
35# Give ourselves a little breathing room from what the server expects.
36DEFAULT_FILE_LIMIT = CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024)
37
38
Mike Frysingercd78a082013-06-26 17:13:04 -040039# How long to wait (in seconds) for a single upload to complete. This has
40# to allow for symbols that are up to CRASH_SERVER_FILE_LIMIT in size.
41UPLOAD_TIMEOUT = 30 * 60
42
43
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040044# Sleep for 200ms in between uploads to avoid DoS'ing symbol server.
45DEFAULT_SLEEP_DELAY = 0.2
46
47
48# Number of seconds to wait before retrying an upload. The delay will double
49# for each subsequent retry of the same symbol file.
50INITIAL_RETRY_DELAY = 1
51
52# Allow up to 7 attempts to upload a symbol file (total delay may be
53# 1+2+4+8+16+32=63 seconds).
54MAX_RETRIES = 6
55
Mike Frysingereb753bf2013-11-22 16:05:35 -050056# Number of total errors, before uploads are no longer attempted.
57# This is used to avoid lots of errors causing unreasonable delays.
58# See the related, but independent, error values below.
59MAX_TOTAL_ERRORS_FOR_RETRY = 30
60
61# A watermark of transient errors which we allow recovery from. If we hit
62# errors infrequently, overall we're probably doing fine. For example, if
63# we have one failure every 100 passes, then we probably don't want to fail
64# right away. But if we hit a string of failures in a row, we want to abort.
65#
66# The watermark starts at 0 (and can never go below that). When this error
67# level is exceeded, we stop uploading. When a failure happens, we add the
68# fail adjustment, and when an upload succeeds, we add the pass adjustment.
69# We want to penalize failures more so that we ramp up when there is a string
70# of them, but then slowly back off as things start working.
71#
72# A quick example:
73# 0.0: Starting point.
74# 0.0: Upload works, so add -0.5, and then clamp to 0.
75# 1.0: Upload fails, so add 1.0.
76# 2.0: Upload fails, so add 1.0.
77# 1.5: Upload works, so add -0.5.
78# 1.0: Upload works, so add -0.5.
79ERROR_WATERMARK = 3.0
80ERROR_ADJUST_FAIL = 1.0
81ERROR_ADJUST_PASS = -0.5
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040082
83
84def SymUpload(sym_file, upload_url):
Mike Frysinger094a2172013-08-14 12:54:35 -040085 """Upload a symbol file to a HTTP server
86
87 The upload is a multipart/form-data POST with the following parameters:
88 code_file: the basename of the module, e.g. "app"
89 code_identifier: the module file's identifier
90 debug_file: the basename of the debugging file, e.g. "app"
91 debug_identifier: the debug file's identifier, usually consisting of
92 the guid and age embedded in the pdb, e.g.
93 "11111111BBBB3333DDDD555555555555F"
94 version: the file version of the module, e.g. "1.2.3.4"
95 product: HTTP-friendly product name
96 os: the operating system that the module was built for
97 cpu: the CPU that the module was built for
98 symbol_file: the contents of the breakpad-format symbol file
99
100 Args:
101 sym_file: The symbol file to upload
102 upload_url: The crash URL to POST the |sym_file| to
103 """
104 sym_header = cros_generate_breakpad_symbols.ReadSymsHeader(sym_file)
105
106 fields = (
107 ('code_file', sym_header.name),
108 ('debug_file', sym_header.name),
109 ('debug_identifier', sym_header.id.replace('-', '')),
110 # Should we set these fields? They aren't critical, but it might be nice?
111 # We'd have to figure out what file this symbol is coming from and what
112 # package provides it ...
113 #('version', None),
114 #('product', 'ChromeOS'),
115 ('os', sym_header.os),
116 ('cpu', sym_header.cpu),
117 poster.encode.MultipartParam.from_file('symbol_file', sym_file),
118 )
119
120 data, headers = poster.encode.multipart_encode(fields)
121 request = urllib2.Request(upload_url, data, headers)
122 request.add_header('User-agent', 'chromite.upload_symbols')
123 urllib2.urlopen(request, timeout=UPLOAD_TIMEOUT)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400124
125
126def TestingSymUpload(sym_file, upload_url):
127 """A stub version of SymUpload for --testing usage"""
128 cmd = ['sym_upload', sym_file, upload_url]
129 # Randomly fail 80% of the time (the retry logic makes this 80%/3 per file).
130 returncode = random.randint(1, 100) <= 80
131 cros_build_lib.Debug('would run (and return %i): %s', returncode,
132 ' '.join(map(repr, cmd)))
133 if returncode:
134 output = 'Failed to send the symbol file.'
135 else:
136 output = 'Successfully sent the symbol file.'
137 result = cros_build_lib.CommandResult(cmd=cmd, error=None, output=output,
138 returncode=returncode)
139 if returncode:
Mike Frysinger094a2172013-08-14 12:54:35 -0400140 raise urllib2.HTTPError(upload_url, 400, 'forced test fail', {}, None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400141 else:
142 return result
143
144
Mike Frysingereb753bf2013-11-22 16:05:35 -0500145def ErrorLimitHit(num_errors, watermark_errors):
146 """See if our error limit has been hit
147
148 Args:
149 num_errors: A multiprocessing.Value of the raw number of failures.
150 watermark_errors: A multiprocessing.Value of the current rate of failures.
151 Returns:
152 True if our error limits have been exceeded.
153 """
154 return ((num_errors is not None and
155 num_errors.value > MAX_TOTAL_ERRORS_FOR_RETRY) or
156 (watermark_errors is not None and
157 watermark_errors.value > ERROR_WATERMARK))
158
159
160def _UpdateCounter(counter, adj):
161 """Update |counter| by |adj|
162
163 Handle atomic updates of |counter|. Also make sure it does not
164 fall below 0.
165
166 Args:
167 counter: A multiprocessing.Value to update
168 adj: The value to add to |counter|
169 """
170 def _Update():
171 clamp = 0 if type(adj) is int else 0.0
172 counter.value = max(clamp, counter.value + adj)
173
174 if hasattr(counter, 'get_lock'):
175 with counter.get_lock():
176 _Update()
177 elif counter is not None:
178 _Update()
179
180
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400181def UploadSymbol(sym_file, upload_url, file_limit=DEFAULT_FILE_LIMIT,
Mike Frysingereb753bf2013-11-22 16:05:35 -0500182 sleep=0, num_errors=None, watermark_errors=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400183 """Upload |sym_file| to |upload_url|
184
185 Args:
186 sym_file: The full path to the breakpad symbol to upload
187 upload_url: The crash server to upload things to
188 file_limit: The max file size of a symbol file before we try to strip it
189 sleep: Number of seconds to sleep before running
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400190 num_errors: An object to update with the error count (needs a .value member)
Mike Frysingereb753bf2013-11-22 16:05:35 -0500191 watermark_errors: An object to track current error behavior (needs a .value)
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400192 Returns:
193 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400194 """
195 if num_errors is None:
196 num_errors = ctypes.c_int()
Mike Frysingereb753bf2013-11-22 16:05:35 -0500197 if ErrorLimitHit(num_errors, watermark_errors):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400198 # Abandon ship! It's on fire! NOoooooooooooOOOoooooo.
199 return 0
200
201 upload_file = sym_file
202
203 if sleep:
204 # Keeps us from DoS-ing the symbol server.
205 time.sleep(sleep)
206
207 cros_build_lib.Debug('uploading %s' % sym_file)
208
209 # Ideally there'd be a tempfile.SpooledNamedTemporaryFile that we could use.
210 with tempfile.NamedTemporaryFile(prefix='upload_symbols',
211 bufsize=0) as temp_sym_file:
212 if file_limit:
213 # If the symbols size is too big, strip out the call frame info. The CFI
214 # is unnecessary for 32bit x86 targets where the frame pointer is used (as
215 # all of ours have) and it accounts for over half the size of the symbols
216 # uploaded.
217 file_size = os.path.getsize(sym_file)
218 if file_size > file_limit:
219 cros_build_lib.Warning('stripping CFI from %s due to size %s > %s',
220 sym_file, file_size, file_limit)
221 temp_sym_file.writelines([x for x in open(sym_file, 'rb').readlines()
222 if not x.startswith('STACK CFI')])
223 upload_file = temp_sym_file.name
224
225 # Hopefully the crash server will let it through. But it probably won't.
226 # Not sure what the best answer is in this case.
227 file_size = os.path.getsize(upload_file)
228 if file_size > CRASH_SERVER_FILE_LIMIT:
229 cros_build_lib.PrintBuildbotStepWarnings()
230 cros_build_lib.Error('upload file %s is awfully large, risking rejection '
231 'by symbol server (%s > %s)', sym_file, file_size,
232 CRASH_SERVER_FILE_LIMIT)
Mike Frysingereb753bf2013-11-22 16:05:35 -0500233 _UpdateCounter(num_errors, 1)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400234
235 # Upload the symbol file.
Mike Frysingereb753bf2013-11-22 16:05:35 -0500236 success = False
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400237 try:
Mike Frysinger9adcfd22013-10-24 12:01:40 -0400238 cros_build_lib.TimedCommand(
239 cros_build_lib.RetryException,
240 (urllib2.HTTPError, urllib2.URLError), MAX_RETRIES, SymUpload,
241 upload_file, upload_url, sleep=INITIAL_RETRY_DELAY,
242 timed_log_msg='upload of %10i bytes took %%s: %s' %
243 (file_size, os.path.basename(sym_file)))
Mike Frysingereb753bf2013-11-22 16:05:35 -0500244 success = True
Mike Frysinger094a2172013-08-14 12:54:35 -0400245 except urllib2.HTTPError as e:
246 cros_build_lib.Warning('could not upload: %s: HTTP %s: %s',
247 os.path.basename(sym_file), e.code, e.reason)
Mike Frysingerc4ab5782013-10-02 18:14:22 -0400248 except urllib2.URLError as e:
249 cros_build_lib.Warning('could not upload: %s: %s',
250 os.path.basename(sym_file), e)
Mike Frysingereb753bf2013-11-22 16:05:35 -0500251 finally:
252 if success:
253 _UpdateCounter(watermark_errors, ERROR_ADJUST_PASS)
254 else:
255 _UpdateCounter(num_errors, 1)
256 _UpdateCounter(watermark_errors, ERROR_ADJUST_FAIL)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400257
258 return num_errors.value
259
260
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500261def SymbolFinder(paths):
262 """Locate symbol files in |paths|
263
264 Args:
265 paths: A list of input paths to walk. Files are returned w/out any checks.
266 Dirs are searched for files that end in ".sym".
267 Returns:
268 Yield every viable sym file.
269 """
270 for p in paths:
271 if os.path.isdir(p):
272 for root, _, files in os.walk(p):
273 for f in files:
274 if f.endswith('.sym'):
275 yield os.path.join(root, f)
276 else:
277 yield p
278
279
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400280def UploadSymbols(board=None, official=False, breakpad_dir=None,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400281 file_limit=DEFAULT_FILE_LIMIT, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500282 upload_count=None, sym_paths=None, root=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400283 """Upload all the generated symbols for |board| to the crash server
284
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400285 You can use in a few ways:
286 * pass |board| to locate all of its symbols
287 * pass |breakpad_dir| to upload all the symbols in there
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500288 * pass |sym_paths| to upload specific symbols (or dirs of symbols)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400289
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400290 Args:
291 board: The board whose symbols we wish to upload
292 official: Use the official symbol server rather than the staging one
293 breakpad_dir: The full path to the breakpad directory where symbols live
294 file_limit: The max file size of a symbol file before we try to strip it
295 sleep: How long to sleep in between uploads
296 upload_count: If set, only upload this many symbols (meant for testing)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500297 sym_paths: Specific symbol files (or dirs of sym files) to upload,
298 otherwise search |breakpad_dir|
Mike Frysinger118d2502013-08-19 03:36:56 -0400299 root: The tree to prefix to |breakpad_dir| (if |breakpad_dir| is not set)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400300 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400301 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400302 """
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400303 if official:
304 upload_url = OFFICIAL_UPLOAD_URL
305 else:
306 cros_build_lib.Warning('unofficial builds upload to the staging server')
307 upload_url = STAGING_UPLOAD_URL
308
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500309 if sym_paths:
310 cros_build_lib.Info('uploading specified symbols to %s', upload_url)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400311 else:
312 if breakpad_dir is None:
Mike Frysinger118d2502013-08-19 03:36:56 -0400313 breakpad_dir = os.path.join(
314 root,
315 cros_generate_breakpad_symbols.FindBreakpadDir(board).lstrip('/'))
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400316 cros_build_lib.Info('uploading all symbols to %s from %s', upload_url,
317 breakpad_dir)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500318 sym_paths = [breakpad_dir]
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400319
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400320 # We need to limit ourselves to one upload at a time to avoid the server
321 # kicking in DoS protection. See these bugs for more details:
322 # http://crbug.com/209442
323 # http://crbug.com/212496
324 bg_errors = multiprocessing.Value('i')
Mike Frysingereb753bf2013-11-22 16:05:35 -0500325 watermark_errors = multiprocessing.Value('f')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400326 with parallel.BackgroundTaskRunner(UploadSymbol, file_limit=file_limit,
327 sleep=sleep, num_errors=bg_errors,
Mike Frysingereb753bf2013-11-22 16:05:35 -0500328 watermark_errors=watermark_errors,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400329 processes=1) as queue:
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500330 for sym_file in SymbolFinder(sym_paths):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400331 if upload_count == 0:
332 break
333
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500334 queue.put([sym_file, upload_url])
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400335
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500336 if upload_count is not None:
337 upload_count -= 1
338 if upload_count == 0:
339 break
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400340
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500341 return bg_errors.value
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400342
343
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400344def main(argv):
345 parser = commandline.ArgumentParser(description=__doc__)
346
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500347 parser.add_argument('sym_paths', type='path', nargs='*', default=None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400348 parser.add_argument('--board', default=None,
349 help='board to build packages for')
350 parser.add_argument('--breakpad_root', type='path', default=None,
351 help='root directory for breakpad symbols')
352 parser.add_argument('--official_build', action='store_true', default=False,
353 help='point to official symbol server')
354 parser.add_argument('--regenerate', action='store_true', default=False,
355 help='regenerate all symbols')
356 parser.add_argument('--upload-count', type=int, default=None,
357 help='only upload # number of symbols')
358 parser.add_argument('--strip_cfi', type=int,
359 default=CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024),
360 help='strip CFI data for files above this size')
361 parser.add_argument('--testing', action='store_true', default=False,
362 help='run in testing mode')
363 parser.add_argument('--yes', action='store_true', default=False,
364 help='answer yes to all prompts')
365
366 opts = parser.parse_args(argv)
367
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500368 if opts.sym_paths:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400369 if opts.regenerate:
370 cros_build_lib.Die('--regenerate may not be used with specific files')
371 else:
372 if opts.board is None:
373 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400374
375 if opts.breakpad_root and opts.regenerate:
376 cros_build_lib.Die('--regenerate may not be used with --breakpad_root')
377
378 if opts.testing:
379 # TODO(build): Kill off --testing mode once unittests are up-to-snuff.
380 cros_build_lib.Info('running in testing mode')
381 # pylint: disable=W0601,W0603
382 global INITIAL_RETRY_DELAY, SymUpload, DEFAULT_SLEEP_DELAY
383 INITIAL_RETRY_DELAY = DEFAULT_SLEEP_DELAY = 0
384 SymUpload = TestingSymUpload
385
386 if not opts.yes:
387 query = textwrap.wrap(textwrap.dedent("""
388 Uploading symbols for an entire Chromium OS build is really only
389 necessary for release builds and in a few cases for developers
390 to debug problems. It will take considerable time to run. For
391 developer debugging purposes, consider instead passing specific
392 files to upload.
393 """), 80)
394 cros_build_lib.Warning('\n%s', '\n'.join(query))
395 if not cros_build_lib.BooleanPrompt(
396 prompt='Are you sure you want to upload all build symbols',
397 default=False):
398 cros_build_lib.Die('better safe than sorry')
399
400 ret = 0
401 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400402 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
403 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400404
405 ret += UploadSymbols(opts.board, official=opts.official_build,
406 breakpad_dir=opts.breakpad_root,
407 file_limit=opts.strip_cfi, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500408 upload_count=opts.upload_count, sym_paths=opts.sym_paths)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400409 if ret:
410 cros_build_lib.Error('encountered %i problem(s)', ret)
411 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
412 # return 0 in case we are a multiple of the mask.
413 ret = 1
414
415 return ret
Mike Frysinger094a2172013-08-14 12:54:35 -0400416
417
418# We need this to run once per process. Do it at module import time as that
419# will let us avoid doing it inline at function call time (see SymUpload) as
420# that func might be called by the multiprocessing module which means we'll
421# do the opener logic multiple times overall. Plus, if you're importing this
422# module, it's a pretty good chance that you're going to need this.
423poster.streaminghttp.register_openers()