blob: d6ace198d398f4e3dcef2f89d05644283ac852bc [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
9for those executables involved)."""
10
11import ctypes
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040012import multiprocessing
13import os
Mike Frysinger094a2172013-08-14 12:54:35 -040014import poster
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040015import random
16import textwrap
17import tempfile
18import time
Mike Frysinger094a2172013-08-14 12:54:35 -040019import urllib2
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040020
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040021from chromite.lib import commandline
22from chromite.lib import cros_build_lib
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040023from chromite.lib import parallel
Mike Frysinger69cb41d2013-08-11 20:08:19 -040024from chromite.scripts import cros_generate_breakpad_symbols
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040025
26
27# URLs used for uploading symbols.
28OFFICIAL_UPLOAD_URL = 'http://clients2.google.com/cr/symbol'
29STAGING_UPLOAD_URL = 'http://clients2.google.com/cr/staging_symbol'
30
31
32# The crash server rejects files that are this big.
33CRASH_SERVER_FILE_LIMIT = 350 * 1024 * 1024
34# Give ourselves a little breathing room from what the server expects.
35DEFAULT_FILE_LIMIT = CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024)
36
37
Mike Frysingercd78a082013-06-26 17:13:04 -040038# How long to wait (in seconds) for a single upload to complete. This has
39# to allow for symbols that are up to CRASH_SERVER_FILE_LIMIT in size.
40UPLOAD_TIMEOUT = 30 * 60
41
42
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040043# Sleep for 200ms in between uploads to avoid DoS'ing symbol server.
44DEFAULT_SLEEP_DELAY = 0.2
45
46
47# Number of seconds to wait before retrying an upload. The delay will double
48# for each subsequent retry of the same symbol file.
49INITIAL_RETRY_DELAY = 1
50
51# Allow up to 7 attempts to upload a symbol file (total delay may be
52# 1+2+4+8+16+32=63 seconds).
53MAX_RETRIES = 6
54
55# Number of total errors, TOTAL_ERROR_COUNT, before retries are no longer
56# attempted. This is used to avoid lots of errors causing unreasonable delays.
57MAX_TOTAL_ERRORS_FOR_RETRY = 3
58
59
60def SymUpload(sym_file, upload_url):
Mike Frysinger094a2172013-08-14 12:54:35 -040061 """Upload a symbol file to a HTTP server
62
63 The upload is a multipart/form-data POST with the following parameters:
64 code_file: the basename of the module, e.g. "app"
65 code_identifier: the module file's identifier
66 debug_file: the basename of the debugging file, e.g. "app"
67 debug_identifier: the debug file's identifier, usually consisting of
68 the guid and age embedded in the pdb, e.g.
69 "11111111BBBB3333DDDD555555555555F"
70 version: the file version of the module, e.g. "1.2.3.4"
71 product: HTTP-friendly product name
72 os: the operating system that the module was built for
73 cpu: the CPU that the module was built for
74 symbol_file: the contents of the breakpad-format symbol file
75
76 Args:
77 sym_file: The symbol file to upload
78 upload_url: The crash URL to POST the |sym_file| to
79 """
80 sym_header = cros_generate_breakpad_symbols.ReadSymsHeader(sym_file)
81
82 fields = (
83 ('code_file', sym_header.name),
84 ('debug_file', sym_header.name),
85 ('debug_identifier', sym_header.id.replace('-', '')),
86 # Should we set these fields? They aren't critical, but it might be nice?
87 # We'd have to figure out what file this symbol is coming from and what
88 # package provides it ...
89 #('version', None),
90 #('product', 'ChromeOS'),
91 ('os', sym_header.os),
92 ('cpu', sym_header.cpu),
93 poster.encode.MultipartParam.from_file('symbol_file', sym_file),
94 )
95
96 data, headers = poster.encode.multipart_encode(fields)
97 request = urllib2.Request(upload_url, data, headers)
98 request.add_header('User-agent', 'chromite.upload_symbols')
99 urllib2.urlopen(request, timeout=UPLOAD_TIMEOUT)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400100
101
102def TestingSymUpload(sym_file, upload_url):
103 """A stub version of SymUpload for --testing usage"""
104 cmd = ['sym_upload', sym_file, upload_url]
105 # Randomly fail 80% of the time (the retry logic makes this 80%/3 per file).
106 returncode = random.randint(1, 100) <= 80
107 cros_build_lib.Debug('would run (and return %i): %s', returncode,
108 ' '.join(map(repr, cmd)))
109 if returncode:
110 output = 'Failed to send the symbol file.'
111 else:
112 output = 'Successfully sent the symbol file.'
113 result = cros_build_lib.CommandResult(cmd=cmd, error=None, output=output,
114 returncode=returncode)
115 if returncode:
Mike Frysinger094a2172013-08-14 12:54:35 -0400116 raise urllib2.HTTPError(upload_url, 400, 'forced test fail', {}, None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400117 else:
118 return result
119
120
121def UploadSymbol(sym_file, upload_url, file_limit=DEFAULT_FILE_LIMIT,
122 sleep=0, num_errors=None):
123 """Upload |sym_file| to |upload_url|
124
125 Args:
126 sym_file: The full path to the breakpad symbol to upload
127 upload_url: The crash server to upload things to
128 file_limit: The max file size of a symbol file before we try to strip it
129 sleep: Number of seconds to sleep before running
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400130 num_errors: An object to update with the error count (needs a .value member)
131 Returns:
132 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400133 """
134 if num_errors is None:
135 num_errors = ctypes.c_int()
136 elif num_errors.value > MAX_TOTAL_ERRORS_FOR_RETRY:
137 # Abandon ship! It's on fire! NOoooooooooooOOOoooooo.
138 return 0
139
140 upload_file = sym_file
141
142 if sleep:
143 # Keeps us from DoS-ing the symbol server.
144 time.sleep(sleep)
145
146 cros_build_lib.Debug('uploading %s' % sym_file)
147
148 # Ideally there'd be a tempfile.SpooledNamedTemporaryFile that we could use.
149 with tempfile.NamedTemporaryFile(prefix='upload_symbols',
150 bufsize=0) as temp_sym_file:
151 if file_limit:
152 # If the symbols size is too big, strip out the call frame info. The CFI
153 # is unnecessary for 32bit x86 targets where the frame pointer is used (as
154 # all of ours have) and it accounts for over half the size of the symbols
155 # uploaded.
156 file_size = os.path.getsize(sym_file)
157 if file_size > file_limit:
158 cros_build_lib.Warning('stripping CFI from %s due to size %s > %s',
159 sym_file, file_size, file_limit)
160 temp_sym_file.writelines([x for x in open(sym_file, 'rb').readlines()
161 if not x.startswith('STACK CFI')])
162 upload_file = temp_sym_file.name
163
164 # Hopefully the crash server will let it through. But it probably won't.
165 # Not sure what the best answer is in this case.
166 file_size = os.path.getsize(upload_file)
167 if file_size > CRASH_SERVER_FILE_LIMIT:
168 cros_build_lib.PrintBuildbotStepWarnings()
169 cros_build_lib.Error('upload file %s is awfully large, risking rejection '
170 'by symbol server (%s > %s)', sym_file, file_size,
171 CRASH_SERVER_FILE_LIMIT)
172 num_errors.value += 1
173
174 # Upload the symbol file.
175 try:
Mike Frysinger9adcfd22013-10-24 12:01:40 -0400176 cros_build_lib.TimedCommand(
177 cros_build_lib.RetryException,
178 (urllib2.HTTPError, urllib2.URLError), MAX_RETRIES, SymUpload,
179 upload_file, upload_url, sleep=INITIAL_RETRY_DELAY,
180 timed_log_msg='upload of %10i bytes took %%s: %s' %
181 (file_size, os.path.basename(sym_file)))
Mike Frysinger094a2172013-08-14 12:54:35 -0400182 except urllib2.HTTPError as e:
183 cros_build_lib.Warning('could not upload: %s: HTTP %s: %s',
184 os.path.basename(sym_file), e.code, e.reason)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400185 num_errors.value += 1
Mike Frysingerc4ab5782013-10-02 18:14:22 -0400186 except urllib2.URLError as e:
187 cros_build_lib.Warning('could not upload: %s: %s',
188 os.path.basename(sym_file), e)
189 num_errors.value += 1
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400190
191 return num_errors.value
192
193
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400194def UploadSymbols(board=None, official=False, breakpad_dir=None,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400195 file_limit=DEFAULT_FILE_LIMIT, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger118d2502013-08-19 03:36:56 -0400196 upload_count=None, sym_files=None, root=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400197 """Upload all the generated symbols for |board| to the crash server
198
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400199 You can use in a few ways:
200 * pass |board| to locate all of its symbols
201 * pass |breakpad_dir| to upload all the symbols in there
202 * pass |sym_files| to upload specific symbols
203
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400204 Args:
205 board: The board whose symbols we wish to upload
206 official: Use the official symbol server rather than the staging one
207 breakpad_dir: The full path to the breakpad directory where symbols live
208 file_limit: The max file size of a symbol file before we try to strip it
209 sleep: How long to sleep in between uploads
210 upload_count: If set, only upload this many symbols (meant for testing)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400211 sym_files: Specific symbol files to upload, otherwise search |breakpad_dir|
Mike Frysinger118d2502013-08-19 03:36:56 -0400212 root: The tree to prefix to |breakpad_dir| (if |breakpad_dir| is not set)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400213 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400214 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400215 """
216 num_errors = 0
217
218 if official:
219 upload_url = OFFICIAL_UPLOAD_URL
220 else:
221 cros_build_lib.Warning('unofficial builds upload to the staging server')
222 upload_url = STAGING_UPLOAD_URL
223
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400224 if sym_files:
225 cros_build_lib.Info('uploading specified symbol files to %s', upload_url)
226 sym_file_sets = [('', '', sym_files)]
227 all_files = True
228 else:
229 if breakpad_dir is None:
Mike Frysinger118d2502013-08-19 03:36:56 -0400230 breakpad_dir = os.path.join(
231 root,
232 cros_generate_breakpad_symbols.FindBreakpadDir(board).lstrip('/'))
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400233 cros_build_lib.Info('uploading all symbols to %s from %s', upload_url,
234 breakpad_dir)
235 sym_file_sets = os.walk(breakpad_dir)
236 all_files = False
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400237
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400238 # We need to limit ourselves to one upload at a time to avoid the server
239 # kicking in DoS protection. See these bugs for more details:
240 # http://crbug.com/209442
241 # http://crbug.com/212496
242 bg_errors = multiprocessing.Value('i')
243 with parallel.BackgroundTaskRunner(UploadSymbol, file_limit=file_limit,
244 sleep=sleep, num_errors=bg_errors,
245 processes=1) as queue:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400246 for root, _, files in sym_file_sets:
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400247 if upload_count == 0:
248 break
249
250 for sym_file in files:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400251 if all_files or sym_file.endswith('.sym'):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400252 sym_file = os.path.join(root, sym_file)
253 queue.put([sym_file, upload_url])
254
255 if upload_count is not None:
256 upload_count -= 1
257 if upload_count == 0:
258 break
259 num_errors += bg_errors.value
260
261 return num_errors
262
263
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400264def main(argv):
265 parser = commandline.ArgumentParser(description=__doc__)
266
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400267 parser.add_argument('sym_files', type='path', nargs='*', default=None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400268 parser.add_argument('--board', default=None,
269 help='board to build packages for')
270 parser.add_argument('--breakpad_root', type='path', default=None,
271 help='root directory for breakpad symbols')
272 parser.add_argument('--official_build', action='store_true', default=False,
273 help='point to official symbol server')
274 parser.add_argument('--regenerate', action='store_true', default=False,
275 help='regenerate all symbols')
276 parser.add_argument('--upload-count', type=int, default=None,
277 help='only upload # number of symbols')
278 parser.add_argument('--strip_cfi', type=int,
279 default=CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024),
280 help='strip CFI data for files above this size')
281 parser.add_argument('--testing', action='store_true', default=False,
282 help='run in testing mode')
283 parser.add_argument('--yes', action='store_true', default=False,
284 help='answer yes to all prompts')
285
286 opts = parser.parse_args(argv)
287
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400288 if opts.sym_files:
289 if opts.regenerate:
290 cros_build_lib.Die('--regenerate may not be used with specific files')
291 else:
292 if opts.board is None:
293 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400294
295 if opts.breakpad_root and opts.regenerate:
296 cros_build_lib.Die('--regenerate may not be used with --breakpad_root')
297
298 if opts.testing:
299 # TODO(build): Kill off --testing mode once unittests are up-to-snuff.
300 cros_build_lib.Info('running in testing mode')
301 # pylint: disable=W0601,W0603
302 global INITIAL_RETRY_DELAY, SymUpload, DEFAULT_SLEEP_DELAY
303 INITIAL_RETRY_DELAY = DEFAULT_SLEEP_DELAY = 0
304 SymUpload = TestingSymUpload
305
306 if not opts.yes:
307 query = textwrap.wrap(textwrap.dedent("""
308 Uploading symbols for an entire Chromium OS build is really only
309 necessary for release builds and in a few cases for developers
310 to debug problems. It will take considerable time to run. For
311 developer debugging purposes, consider instead passing specific
312 files to upload.
313 """), 80)
314 cros_build_lib.Warning('\n%s', '\n'.join(query))
315 if not cros_build_lib.BooleanPrompt(
316 prompt='Are you sure you want to upload all build symbols',
317 default=False):
318 cros_build_lib.Die('better safe than sorry')
319
320 ret = 0
321 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400322 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
323 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400324
325 ret += UploadSymbols(opts.board, official=opts.official_build,
326 breakpad_dir=opts.breakpad_root,
327 file_limit=opts.strip_cfi, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400328 upload_count=opts.upload_count, sym_files=opts.sym_files)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400329 if ret:
330 cros_build_lib.Error('encountered %i problem(s)', ret)
331 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
332 # return 0 in case we are a multiple of the mask.
333 ret = 1
334
335 return ret
Mike Frysinger094a2172013-08-14 12:54:35 -0400336
337
338# We need this to run once per process. Do it at module import time as that
339# will let us avoid doing it inline at function call time (see SymUpload) as
340# that func might be called by the multiprocessing module which means we'll
341# do the opener logic multiple times overall. Plus, if you're importing this
342# module, it's a pretty good chance that you're going to need this.
343poster.streaminghttp.register_openers()