blob: 2d79f75a94d1ce80f6069a823f85b6f79158c8ad [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
12import logging
13import multiprocessing
14import os
15import random
16import textwrap
17import tempfile
18import time
19
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040020from chromite.lib import commandline
21from chromite.lib import cros_build_lib
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040022from chromite.lib import parallel
Mike Frysinger69cb41d2013-08-11 20:08:19 -040023from chromite.scripts import cros_generate_breakpad_symbols
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040024
25
26# URLs used for uploading symbols.
27OFFICIAL_UPLOAD_URL = 'http://clients2.google.com/cr/symbol'
28STAGING_UPLOAD_URL = 'http://clients2.google.com/cr/staging_symbol'
29
30
31# The crash server rejects files that are this big.
32CRASH_SERVER_FILE_LIMIT = 350 * 1024 * 1024
33# Give ourselves a little breathing room from what the server expects.
34DEFAULT_FILE_LIMIT = CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024)
35
36
Mike Frysingercd78a082013-06-26 17:13:04 -040037# How long to wait (in seconds) for a single upload to complete. This has
38# to allow for symbols that are up to CRASH_SERVER_FILE_LIMIT in size.
39UPLOAD_TIMEOUT = 30 * 60
40
41
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040042# Sleep for 200ms in between uploads to avoid DoS'ing symbol server.
43DEFAULT_SLEEP_DELAY = 0.2
44
45
46# Number of seconds to wait before retrying an upload. The delay will double
47# for each subsequent retry of the same symbol file.
48INITIAL_RETRY_DELAY = 1
49
50# Allow up to 7 attempts to upload a symbol file (total delay may be
51# 1+2+4+8+16+32=63 seconds).
52MAX_RETRIES = 6
53
54# Number of total errors, TOTAL_ERROR_COUNT, before retries are no longer
55# attempted. This is used to avoid lots of errors causing unreasonable delays.
56MAX_TOTAL_ERRORS_FOR_RETRY = 3
57
58
59def SymUpload(sym_file, upload_url):
60 """Run breakpad sym_upload helper"""
61 # TODO(vapier): Rewrite to use native python HTTP libraries. This tool
62 # reads the sym_file and does a HTTP post to URL with a few fields set.
63 # See the tiny breakpad/tools/linux/symupload/sym_upload.cc for details.
64 cmd = ['sym_upload', sym_file, upload_url]
Mike Frysingercd78a082013-06-26 17:13:04 -040065 with cros_build_lib.SubCommandTimeout(UPLOAD_TIMEOUT):
66 return cros_build_lib.RunCommandCaptureOutput(
67 cmd, debug_level=logging.DEBUG)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -040068
69
70def TestingSymUpload(sym_file, upload_url):
71 """A stub version of SymUpload for --testing usage"""
72 cmd = ['sym_upload', sym_file, upload_url]
73 # Randomly fail 80% of the time (the retry logic makes this 80%/3 per file).
74 returncode = random.randint(1, 100) <= 80
75 cros_build_lib.Debug('would run (and return %i): %s', returncode,
76 ' '.join(map(repr, cmd)))
77 if returncode:
78 output = 'Failed to send the symbol file.'
79 else:
80 output = 'Successfully sent the symbol file.'
81 result = cros_build_lib.CommandResult(cmd=cmd, error=None, output=output,
82 returncode=returncode)
83 if returncode:
84 raise cros_build_lib.RunCommandError('forced test fail', result)
85 else:
86 return result
87
88
89def UploadSymbol(sym_file, upload_url, file_limit=DEFAULT_FILE_LIMIT,
90 sleep=0, num_errors=None):
91 """Upload |sym_file| to |upload_url|
92
93 Args:
94 sym_file: The full path to the breakpad symbol to upload
95 upload_url: The crash server to upload things to
96 file_limit: The max file size of a symbol file before we try to strip it
97 sleep: Number of seconds to sleep before running
Mike Frysinger69cb41d2013-08-11 20:08:19 -040098 num_errors: An object to update with the error count (needs a .value member)
99 Returns:
100 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400101 """
102 if num_errors is None:
103 num_errors = ctypes.c_int()
104 elif num_errors.value > MAX_TOTAL_ERRORS_FOR_RETRY:
105 # Abandon ship! It's on fire! NOoooooooooooOOOoooooo.
106 return 0
107
108 upload_file = sym_file
109
110 if sleep:
111 # Keeps us from DoS-ing the symbol server.
112 time.sleep(sleep)
113
114 cros_build_lib.Debug('uploading %s' % sym_file)
115
116 # Ideally there'd be a tempfile.SpooledNamedTemporaryFile that we could use.
117 with tempfile.NamedTemporaryFile(prefix='upload_symbols',
118 bufsize=0) as temp_sym_file:
119 if file_limit:
120 # If the symbols size is too big, strip out the call frame info. The CFI
121 # is unnecessary for 32bit x86 targets where the frame pointer is used (as
122 # all of ours have) and it accounts for over half the size of the symbols
123 # uploaded.
124 file_size = os.path.getsize(sym_file)
125 if file_size > file_limit:
126 cros_build_lib.Warning('stripping CFI from %s due to size %s > %s',
127 sym_file, file_size, file_limit)
128 temp_sym_file.writelines([x for x in open(sym_file, 'rb').readlines()
129 if not x.startswith('STACK CFI')])
130 upload_file = temp_sym_file.name
131
132 # Hopefully the crash server will let it through. But it probably won't.
133 # Not sure what the best answer is in this case.
134 file_size = os.path.getsize(upload_file)
135 if file_size > CRASH_SERVER_FILE_LIMIT:
136 cros_build_lib.PrintBuildbotStepWarnings()
137 cros_build_lib.Error('upload file %s is awfully large, risking rejection '
138 'by symbol server (%s > %s)', sym_file, file_size,
139 CRASH_SERVER_FILE_LIMIT)
140 num_errors.value += 1
141
142 # Upload the symbol file.
143 try:
144 cros_build_lib.RetryCommand(SymUpload, MAX_RETRIES, upload_file,
145 upload_url, sleep=INITIAL_RETRY_DELAY)
146 cros_build_lib.Info('successfully uploaded %10i bytes: %s', file_size,
147 os.path.basename(sym_file))
148 except cros_build_lib.RunCommandError as e:
149 cros_build_lib.Warning('could not upload: %s:\n{stdout} %s\n{stderr} %s',
150 os.path.basename(sym_file), e.result.output,
151 e.result.error)
152 num_errors.value += 1
153
154 return num_errors.value
155
156
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400157def UploadSymbols(board=None, official=False, breakpad_dir=None,
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400158 file_limit=DEFAULT_FILE_LIMIT, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400159 upload_count=None, sym_files=None):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400160 """Upload all the generated symbols for |board| to the crash server
161
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400162 You can use in a few ways:
163 * pass |board| to locate all of its symbols
164 * pass |breakpad_dir| to upload all the symbols in there
165 * pass |sym_files| to upload specific symbols
166
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400167 Args:
168 board: The board whose symbols we wish to upload
169 official: Use the official symbol server rather than the staging one
170 breakpad_dir: The full path to the breakpad directory where symbols live
171 file_limit: The max file size of a symbol file before we try to strip it
172 sleep: How long to sleep in between uploads
173 upload_count: If set, only upload this many symbols (meant for testing)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400174 sym_files: Specific symbol files to upload, otherwise search |breakpad_dir|
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400175 Returns:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400176 The number of errors that were encountered.
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400177 """
178 num_errors = 0
179
180 if official:
181 upload_url = OFFICIAL_UPLOAD_URL
182 else:
183 cros_build_lib.Warning('unofficial builds upload to the staging server')
184 upload_url = STAGING_UPLOAD_URL
185
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400186 if sym_files:
187 cros_build_lib.Info('uploading specified symbol files to %s', upload_url)
188 sym_file_sets = [('', '', sym_files)]
189 all_files = True
190 else:
191 if breakpad_dir is None:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400192 breakpad_dir = cros_generate_breakpad_symbols.FindBreakpadDir(board)
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400193 cros_build_lib.Info('uploading all symbols to %s from %s', upload_url,
194 breakpad_dir)
195 sym_file_sets = os.walk(breakpad_dir)
196 all_files = False
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400197
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400198 # We need to limit ourselves to one upload at a time to avoid the server
199 # kicking in DoS protection. See these bugs for more details:
200 # http://crbug.com/209442
201 # http://crbug.com/212496
202 bg_errors = multiprocessing.Value('i')
203 with parallel.BackgroundTaskRunner(UploadSymbol, file_limit=file_limit,
204 sleep=sleep, num_errors=bg_errors,
205 processes=1) as queue:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400206 for root, _, files in sym_file_sets:
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400207 if upload_count == 0:
208 break
209
210 for sym_file in files:
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400211 if all_files or sym_file.endswith('.sym'):
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400212 sym_file = os.path.join(root, sym_file)
213 queue.put([sym_file, upload_url])
214
215 if upload_count is not None:
216 upload_count -= 1
217 if upload_count == 0:
218 break
219 num_errors += bg_errors.value
220
221 return num_errors
222
223
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400224def main(argv):
225 parser = commandline.ArgumentParser(description=__doc__)
226
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400227 parser.add_argument('sym_files', type='path', nargs='*', default=None)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400228 parser.add_argument('--board', default=None,
229 help='board to build packages for')
230 parser.add_argument('--breakpad_root', type='path', default=None,
231 help='root directory for breakpad symbols')
232 parser.add_argument('--official_build', action='store_true', default=False,
233 help='point to official symbol server')
234 parser.add_argument('--regenerate', action='store_true', default=False,
235 help='regenerate all symbols')
236 parser.add_argument('--upload-count', type=int, default=None,
237 help='only upload # number of symbols')
238 parser.add_argument('--strip_cfi', type=int,
239 default=CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024),
240 help='strip CFI data for files above this size')
241 parser.add_argument('--testing', action='store_true', default=False,
242 help='run in testing mode')
243 parser.add_argument('--yes', action='store_true', default=False,
244 help='answer yes to all prompts')
245
246 opts = parser.parse_args(argv)
247
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400248 if opts.sym_files:
249 if opts.regenerate:
250 cros_build_lib.Die('--regenerate may not be used with specific files')
251 else:
252 if opts.board is None:
253 cros_build_lib.Die('--board is required')
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400254
255 if opts.breakpad_root and opts.regenerate:
256 cros_build_lib.Die('--regenerate may not be used with --breakpad_root')
257
258 if opts.testing:
259 # TODO(build): Kill off --testing mode once unittests are up-to-snuff.
260 cros_build_lib.Info('running in testing mode')
261 # pylint: disable=W0601,W0603
262 global INITIAL_RETRY_DELAY, SymUpload, DEFAULT_SLEEP_DELAY
263 INITIAL_RETRY_DELAY = DEFAULT_SLEEP_DELAY = 0
264 SymUpload = TestingSymUpload
265
266 if not opts.yes:
267 query = textwrap.wrap(textwrap.dedent("""
268 Uploading symbols for an entire Chromium OS build is really only
269 necessary for release builds and in a few cases for developers
270 to debug problems. It will take considerable time to run. For
271 developer debugging purposes, consider instead passing specific
272 files to upload.
273 """), 80)
274 cros_build_lib.Warning('\n%s', '\n'.join(query))
275 if not cros_build_lib.BooleanPrompt(
276 prompt='Are you sure you want to upload all build symbols',
277 default=False):
278 cros_build_lib.Die('better safe than sorry')
279
280 ret = 0
281 if opts.regenerate:
Mike Frysinger69cb41d2013-08-11 20:08:19 -0400282 ret += cros_generate_breakpad_symbols.GenerateBreakpadSymbols(
283 opts.board, breakpad_dir=opts.breakpad_root)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400284
285 ret += UploadSymbols(opts.board, official=opts.official_build,
286 breakpad_dir=opts.breakpad_root,
287 file_limit=opts.strip_cfi, sleep=DEFAULT_SLEEP_DELAY,
Mike Frysinger9dcf9ae2013-08-10 15:17:09 -0400288 upload_count=opts.upload_count, sym_files=opts.sym_files)
Mike Frysingerd5fcb3a2013-05-30 21:10:50 -0400289 if ret:
290 cros_build_lib.Error('encountered %i problem(s)', ret)
291 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
292 # return 0 in case we are a multiple of the mask.
293 ret = 1
294
295 return ret