blob: dca2780803d824a33b8467d5f1bd92559fd6169e [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
56# Number of total errors, TOTAL_ERROR_COUNT, before retries are no longer
57# attempted. This is used to avoid lots of errors causing unreasonable delays.
58MAX_TOTAL_ERRORS_FOR_RETRY = 3
59
60
61def SymUpload(sym_file, upload_url):
Mike Frysinger094a2172013-08-14 12:54:35 -040062 """Upload a symbol file to a HTTP server
63
64 The upload is a multipart/form-data POST with the following parameters:
65 code_file: the basename of the module, e.g. "app"
66 code_identifier: the module file's identifier
67 debug_file: the basename of the debugging file, e.g. "app"
68 debug_identifier: the debug file's identifier, usually consisting of
69 the guid and age embedded in the pdb, e.g.
70 "11111111BBBB3333DDDD555555555555F"
71 version: the file version of the module, e.g. "1.2.3.4"
72 product: HTTP-friendly product name
73 os: the operating system that the module was built for
74 cpu: the CPU that the module was built for
75 symbol_file: the contents of the breakpad-format symbol file
76
77 Args:
78 sym_file: The symbol file to upload
79 upload_url: The crash URL to POST the |sym_file| to
80 """
81 sym_header = cros_generate_breakpad_symbols.ReadSymsHeader(sym_file)
82
83 fields = (
84 ('code_file', sym_header.name),
85 ('debug_file', sym_header.name),
86 ('debug_identifier', sym_header.id.replace('-', '')),
87 # Should we set these fields? They aren't critical, but it might be nice?
88 # We'd have to figure out what file this symbol is coming from and what
89 # package provides it ...
90 #('version', None),
91 #('product', 'ChromeOS'),
92 ('os', sym_header.os),
93 ('cpu', sym_header.cpu),
94 poster.encode.MultipartParam.from_file('symbol_file', sym_file),
95 )
96
97 data, headers = poster.encode.multipart_encode(fields)
98 request = urllib2.Request(upload_url, data, headers)
99 request.add_header('User-agent', 'chromite.upload_symbols')
100 urllib2.urlopen(request, timeout=UPLOAD_TIMEOUT)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400101
102
103def TestingSymUpload(sym_file, upload_url):
104 """A stub version of SymUpload for --testing usage"""
105 cmd = ['sym_upload', sym_file, upload_url]
106 # Randomly fail 80% of the time (the retry logic makes this 80%/3 per file).
107 returncode = random.randint(1, 100) <= 80
108 cros_build_lib.Debug('would run (and return %i): %s', returncode,
109 ' '.join(map(repr, cmd)))
110 if returncode:
111 output = 'Failed to send the symbol file.'
112 else:
113 output = 'Successfully sent the symbol file.'
114 result = cros_build_lib.CommandResult(cmd=cmd, error=None, output=output,
115 returncode=returncode)
116 if returncode:
Mike Frysinger094a2172013-08-14 12:54:35 -0400117 raise urllib2.HTTPError(upload_url, 400, 'forced test fail', {}, None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400118 else:
119 return result
120
121
122def UploadSymbol(sym_file, upload_url, file_limit=DEFAULT_FILE_LIMIT,
123 sleep=0, num_errors=None):
124 """Upload |sym_file| to |upload_url|
125
126 Args:
127 sym_file: The full path to the breakpad symbol to upload
128 upload_url: The crash server to upload things to
129 file_limit: The max file size of a symbol file before we try to strip it
130 sleep: Number of seconds to sleep before running
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400131 num_errors: An object to update with the error count (needs a .value member)
132 Returns:
133 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400134 """
135 if num_errors is None:
136 num_errors = ctypes.c_int()
137 elif num_errors.value > MAX_TOTAL_ERRORS_FOR_RETRY:
138 # Abandon ship! It's on fire! NOoooooooooooOOOoooooo.
139 return 0
140
141 upload_file = sym_file
142
143 if sleep:
144 # Keeps us from DoS-ing the symbol server.
145 time.sleep(sleep)
146
147 cros_build_lib.Debug('uploading %s' % sym_file)
148
149 # Ideally there'd be a tempfile.SpooledNamedTemporaryFile that we could use.
150 with tempfile.NamedTemporaryFile(prefix='upload_symbols',
151 bufsize=0) as temp_sym_file:
152 if file_limit:
153 # If the symbols size is too big, strip out the call frame info. The CFI
154 # is unnecessary for 32bit x86 targets where the frame pointer is used (as
155 # all of ours have) and it accounts for over half the size of the symbols
156 # uploaded.
157 file_size = os.path.getsize(sym_file)
158 if file_size > file_limit:
159 cros_build_lib.Warning('stripping CFI from %s due to size %s > %s',
160 sym_file, file_size, file_limit)
161 temp_sym_file.writelines([x for x in open(sym_file, 'rb').readlines()
162 if not x.startswith('STACK CFI')])
163 upload_file = temp_sym_file.name
164
165 # Hopefully the crash server will let it through. But it probably won't.
166 # Not sure what the best answer is in this case.
167 file_size = os.path.getsize(upload_file)
168 if file_size > CRASH_SERVER_FILE_LIMIT:
169 cros_build_lib.PrintBuildbotStepWarnings()
170 cros_build_lib.Error('upload file %s is awfully large, risking rejection '
171 'by symbol server (%s > %s)', sym_file, file_size,
172 CRASH_SERVER_FILE_LIMIT)
173 num_errors.value += 1
174
175 # Upload the symbol file.
176 try:
Mike Frysinger9adcfd22013-10-24 12:01:40 -0400177 cros_build_lib.TimedCommand(
178 cros_build_lib.RetryException,
179 (urllib2.HTTPError, urllib2.URLError), MAX_RETRIES, SymUpload,
180 upload_file, upload_url, sleep=INITIAL_RETRY_DELAY,
181 timed_log_msg='upload of %10i bytes took %%s: %s' %
182 (file_size, os.path.basename(sym_file)))
Mike Frysinger094a2172013-08-14 12:54:35 -0400183 except urllib2.HTTPError as e:
184 cros_build_lib.Warning('could not upload: %s: HTTP %s: %s',
185 os.path.basename(sym_file), e.code, e.reason)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400186 num_errors.value += 1
Mike Frysingerc4ab5782013-10-02 18:14:22 -0400187 except urllib2.URLError as e:
188 cros_build_lib.Warning('could not upload: %s: %s',
189 os.path.basename(sym_file), e)
190 num_errors.value += 1
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400191
192 return num_errors.value
193
194
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500195def SymbolFinder(paths):
196 """Locate symbol files in |paths|
197
198 Args:
199 paths: A list of input paths to walk. Files are returned w/out any checks.
200 Dirs are searched for files that end in ".sym".
201 Returns:
202 Yield every viable sym file.
203 """
204 for p in paths:
205 if os.path.isdir(p):
206 for root, _, files in os.walk(p):
207 for f in files:
208 if f.endswith('.sym'):
209 yield os.path.join(root, f)
210 else:
211 yield p
212
213
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400214def UploadSymbols(board=None, official=False, breakpad_dir=None,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400215 file_limit=DEFAULT_FILE_LIMIT, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500216 upload_count=None, sym_paths=None, root=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400217 """Upload all the generated symbols for |board| to the crash server
218
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400219 You can use in a few ways:
220 * pass |board| to locate all of its symbols
221 * pass |breakpad_dir| to upload all the symbols in there
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500222 * pass |sym_paths| to upload specific symbols (or dirs of symbols)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400223
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400224 Args:
225 board: The board whose symbols we wish to upload
226 official: Use the official symbol server rather than the staging one
227 breakpad_dir: The full path to the breakpad directory where symbols live
228 file_limit: The max file size of a symbol file before we try to strip it
229 sleep: How long to sleep in between uploads
230 upload_count: If set, only upload this many symbols (meant for testing)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500231 sym_paths: Specific symbol files (or dirs of sym files) to upload,
232 otherwise search |breakpad_dir|
Mike Frysinger118d2502013-08-19 03:36:56 -0400233 root: The tree to prefix to |breakpad_dir| (if |breakpad_dir| is not set)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400234 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400235 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400236 """
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400237 if official:
238 upload_url = OFFICIAL_UPLOAD_URL
239 else:
240 cros_build_lib.Warning('unofficial builds upload to the staging server')
241 upload_url = STAGING_UPLOAD_URL
242
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500243 if sym_paths:
244 cros_build_lib.Info('uploading specified symbols to %s', upload_url)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400245 else:
246 if breakpad_dir is None:
Mike Frysinger118d2502013-08-19 03:36:56 -0400247 breakpad_dir = os.path.join(
248 root,
249 cros_generate_breakpad_symbols.FindBreakpadDir(board).lstrip('/'))
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400250 cros_build_lib.Info('uploading all symbols to %s from %s', upload_url,
251 breakpad_dir)
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500252 sym_paths = [breakpad_dir]
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400253
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400254 # We need to limit ourselves to one upload at a time to avoid the server
255 # kicking in DoS protection. See these bugs for more details:
256 # http://crbug.com/209442
257 # http://crbug.com/212496
258 bg_errors = multiprocessing.Value('i')
259 with parallel.BackgroundTaskRunner(UploadSymbol, file_limit=file_limit,
260 sleep=sleep, num_errors=bg_errors,
261 processes=1) as queue:
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500262 for sym_file in SymbolFinder(sym_paths):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400263 if upload_count == 0:
264 break
265
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500266 queue.put([sym_file, upload_url])
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400267
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500268 if upload_count is not None:
269 upload_count -= 1
270 if upload_count == 0:
271 break
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400272
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500273 return bg_errors.value
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400274
275
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400276def main(argv):
277 parser = commandline.ArgumentParser(description=__doc__)
278
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500279 parser.add_argument('sym_paths', type='path', nargs='*', default=None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400280 parser.add_argument('--board', default=None,
281 help='board to build packages for')
282 parser.add_argument('--breakpad_root', type='path', default=None,
283 help='root directory for breakpad symbols')
284 parser.add_argument('--official_build', action='store_true', default=False,
285 help='point to official symbol server')
286 parser.add_argument('--regenerate', action='store_true', default=False,
287 help='regenerate all symbols')
288 parser.add_argument('--upload-count', type=int, default=None,
289 help='only upload # number of symbols')
290 parser.add_argument('--strip_cfi', type=int,
291 default=CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024),
292 help='strip CFI data for files above this size')
293 parser.add_argument('--testing', action='store_true', default=False,
294 help='run in testing mode')
295 parser.add_argument('--yes', action='store_true', default=False,
296 help='answer yes to all prompts')
297
298 opts = parser.parse_args(argv)
299
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500300 if opts.sym_paths:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400301 if opts.regenerate:
302 cros_build_lib.Die('--regenerate may not be used with specific files')
303 else:
304 if opts.board is None:
305 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400306
307 if opts.breakpad_root and opts.regenerate:
308 cros_build_lib.Die('--regenerate may not be used with --breakpad_root')
309
310 if opts.testing:
311 # TODO(build): Kill off --testing mode once unittests are up-to-snuff.
312 cros_build_lib.Info('running in testing mode')
313 # pylint: disable=W0601,W0603
314 global INITIAL_RETRY_DELAY, SymUpload, DEFAULT_SLEEP_DELAY
315 INITIAL_RETRY_DELAY = DEFAULT_SLEEP_DELAY = 0
316 SymUpload = TestingSymUpload
317
318 if not opts.yes:
319 query = textwrap.wrap(textwrap.dedent("""
320 Uploading symbols for an entire Chromium OS build is really only
321 necessary for release builds and in a few cases for developers
322 to debug problems. It will take considerable time to run. For
323 developer debugging purposes, consider instead passing specific
324 files to upload.
325 """), 80)
326 cros_build_lib.Warning('\n%s', '\n'.join(query))
327 if not cros_build_lib.BooleanPrompt(
328 prompt='Are you sure you want to upload all build symbols',
329 default=False):
330 cros_build_lib.Die('better safe than sorry')
331
332 ret = 0
333 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400334 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
335 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400336
337 ret += UploadSymbols(opts.board, official=opts.official_build,
338 breakpad_dir=opts.breakpad_root,
339 file_limit=opts.strip_cfi, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9b2ff5c2013-11-22 10:01:12 -0500340 upload_count=opts.upload_count, sym_paths=opts.sym_paths)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400341 if ret:
342 cros_build_lib.Error('encountered %i problem(s)', ret)
343 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
344 # return 0 in case we are a multiple of the mask.
345 ret = 1
346
347 return ret
Mike Frysinger094a2172013-08-14 12:54:35 -0400348
349
350# We need this to run once per process. Do it at module import time as that
351# will let us avoid doing it inline at function call time (see SymUpload) as
352# that func might be called by the multiprocessing module which means we'll
353# do the opener logic multiple times overall. Plus, if you're importing this
354# module, it's a pretty good chance that you're going to need this.
355poster.streaminghttp.register_openers()