blob: 2b89a83d541a923c658afd23e70f126bbb25a6e5 [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
20from chromite.buildbot import constants
21from chromite.lib import commandline
22from chromite.lib import cros_build_lib
23from chromite.lib import osutils
24from chromite.lib import parallel
25
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
38# Sleep for 200ms in between uploads to avoid DoS'ing symbol server.
39DEFAULT_SLEEP_DELAY = 0.2
40
41
42# Number of seconds to wait before retrying an upload. The delay will double
43# for each subsequent retry of the same symbol file.
44INITIAL_RETRY_DELAY = 1
45
46# Allow up to 7 attempts to upload a symbol file (total delay may be
47# 1+2+4+8+16+32=63 seconds).
48MAX_RETRIES = 6
49
50# Number of total errors, TOTAL_ERROR_COUNT, before retries are no longer
51# attempted. This is used to avoid lots of errors causing unreasonable delays.
52MAX_TOTAL_ERRORS_FOR_RETRY = 3
53
54
55def SymUpload(sym_file, upload_url):
56 """Run breakpad sym_upload helper"""
57 # TODO(vapier): Rewrite to use native python HTTP libraries. This tool
58 # reads the sym_file and does a HTTP post to URL with a few fields set.
59 # See the tiny breakpad/tools/linux/symupload/sym_upload.cc for details.
60 cmd = ['sym_upload', sym_file, upload_url]
61 return cros_build_lib.RunCommandCaptureOutput(cmd)
62
63
64def TestingSymUpload(sym_file, upload_url):
65 """A stub version of SymUpload for --testing usage"""
66 cmd = ['sym_upload', sym_file, upload_url]
67 # Randomly fail 80% of the time (the retry logic makes this 80%/3 per file).
68 returncode = random.randint(1, 100) <= 80
69 cros_build_lib.Debug('would run (and return %i): %s', returncode,
70 ' '.join(map(repr, cmd)))
71 if returncode:
72 output = 'Failed to send the symbol file.'
73 else:
74 output = 'Successfully sent the symbol file.'
75 result = cros_build_lib.CommandResult(cmd=cmd, error=None, output=output,
76 returncode=returncode)
77 if returncode:
78 raise cros_build_lib.RunCommandError('forced test fail', result)
79 else:
80 return result
81
82
83def UploadSymbol(sym_file, upload_url, file_limit=DEFAULT_FILE_LIMIT,
84 sleep=0, num_errors=None):
85 """Upload |sym_file| to |upload_url|
86
87 Args:
88 sym_file: The full path to the breakpad symbol to upload
89 upload_url: The crash server to upload things to
90 file_limit: The max file size of a symbol file before we try to strip it
91 sleep: Number of seconds to sleep before running
92 """
93 if num_errors is None:
94 num_errors = ctypes.c_int()
95 elif num_errors.value > MAX_TOTAL_ERRORS_FOR_RETRY:
96 # Abandon ship! It's on fire! NOoooooooooooOOOoooooo.
97 return 0
98
99 upload_file = sym_file
100
101 if sleep:
102 # Keeps us from DoS-ing the symbol server.
103 time.sleep(sleep)
104
105 cros_build_lib.Debug('uploading %s' % sym_file)
106
107 # Ideally there'd be a tempfile.SpooledNamedTemporaryFile that we could use.
108 with tempfile.NamedTemporaryFile(prefix='upload_symbols',
109 bufsize=0) as temp_sym_file:
110 if file_limit:
111 # If the symbols size is too big, strip out the call frame info. The CFI
112 # is unnecessary for 32bit x86 targets where the frame pointer is used (as
113 # all of ours have) and it accounts for over half the size of the symbols
114 # uploaded.
115 file_size = os.path.getsize(sym_file)
116 if file_size > file_limit:
117 cros_build_lib.Warning('stripping CFI from %s due to size %s > %s',
118 sym_file, file_size, file_limit)
119 temp_sym_file.writelines([x for x in open(sym_file, 'rb').readlines()
120 if not x.startswith('STACK CFI')])
121 upload_file = temp_sym_file.name
122
123 # Hopefully the crash server will let it through. But it probably won't.
124 # Not sure what the best answer is in this case.
125 file_size = os.path.getsize(upload_file)
126 if file_size > CRASH_SERVER_FILE_LIMIT:
127 cros_build_lib.PrintBuildbotStepWarnings()
128 cros_build_lib.Error('upload file %s is awfully large, risking rejection '
129 'by symbol server (%s > %s)', sym_file, file_size,
130 CRASH_SERVER_FILE_LIMIT)
131 num_errors.value += 1
132
133 # Upload the symbol file.
134 try:
135 cros_build_lib.RetryCommand(SymUpload, MAX_RETRIES, upload_file,
136 upload_url, sleep=INITIAL_RETRY_DELAY)
137 cros_build_lib.Info('successfully uploaded %10i bytes: %s', file_size,
138 os.path.basename(sym_file))
139 except cros_build_lib.RunCommandError as e:
140 cros_build_lib.Warning('could not upload: %s:\n{stdout} %s\n{stderr} %s',
141 os.path.basename(sym_file), e.result.output,
142 e.result.error)
143 num_errors.value += 1
144
145 return num_errors.value
146
147
148def UploadSymbols(board, official=False, breakpad_dir=None,
149 file_limit=DEFAULT_FILE_LIMIT, sleep=DEFAULT_SLEEP_DELAY,
150 upload_count=None):
151 """Upload all the generated symbols for |board| to the crash server
152
153 Args:
154 board: The board whose symbols we wish to upload
155 official: Use the official symbol server rather than the staging one
156 breakpad_dir: The full path to the breakpad directory where symbols live
157 file_limit: The max file size of a symbol file before we try to strip it
158 sleep: How long to sleep in between uploads
159 upload_count: If set, only upload this many symbols (meant for testing)
160 Returns:
161 False if some errors were encountered, True otherwise.
162 """
163 num_errors = 0
164
165 if official:
166 upload_url = OFFICIAL_UPLOAD_URL
167 else:
168 cros_build_lib.Warning('unofficial builds upload to the staging server')
169 upload_url = STAGING_UPLOAD_URL
170
171 if breakpad_dir is None:
172 breakpad_dir = FindBreakpadDir(board)
173 cros_build_lib.Info('uploading symbols to %s from %s', upload_url,
174 breakpad_dir)
175
176 cros_build_lib.Info('uploading all breakpad symbol files')
177 # We need to limit ourselves to one upload at a time to avoid the server
178 # kicking in DoS protection. See these bugs for more details:
179 # http://crbug.com/209442
180 # http://crbug.com/212496
181 bg_errors = multiprocessing.Value('i')
182 with parallel.BackgroundTaskRunner(UploadSymbol, file_limit=file_limit,
183 sleep=sleep, num_errors=bg_errors,
184 processes=1) as queue:
185 for root, _, files in os.walk(breakpad_dir):
186 if upload_count == 0:
187 break
188
189 for sym_file in files:
190 if sym_file.endswith('.sym'):
191 sym_file = os.path.join(root, sym_file)
192 queue.put([sym_file, upload_url])
193
194 if upload_count is not None:
195 upload_count -= 1
196 if upload_count == 0:
197 break
198 num_errors += bg_errors.value
199
200 return num_errors
201
202
203def GenerateBreakpadSymbols(board, breakpad_dir=None):
204 """Generate all the symbols for this board
205
206 Note: this should be merged with buildbot_commands.GenerateBreakpadSymbols()
207 once we rewrite cros_generate_breakpad_symbols in python.
208
209 Args:
210 board: The board whose symbols we wish to generate
211 breakpad_dir: The full path to the breakpad directory where symbols live
212 """
213 if breakpad_dir is None:
214 breakpad_dir = FindBreakpadDir(board)
215
216 cros_build_lib.Info('clearing out %s', breakpad_dir)
217 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
218
219 cros_build_lib.Info('generating all breakpad symbol files')
220 cmd = [os.path.join(constants.CROSUTILS_DIR,
221 'cros_generate_breakpad_symbols'),
222 '--board', board]
223 if cros_build_lib.logger.getEffectiveLevel() < logging.INFO:
224 cmd += ['--verbose']
225 result = cros_build_lib.RunCommand(cmd, error_code_ok=True)
226 if result.returncode:
227 cros_build_lib.Warning('errors hit while generating symbols; '
228 'uploading anyways')
229 return 1
230
231 return 0
232
233
234def FindBreakpadDir(board):
235 """Given a |board|, return the path to the breakpad dir for it"""
236 return os.path.join('/build', board, 'usr', 'lib', 'debug', 'breakpad')
237
238
239def main(argv):
240 parser = commandline.ArgumentParser(description=__doc__)
241
242 parser.add_argument('--board', default=None,
243 help='board to build packages for')
244 parser.add_argument('--breakpad_root', type='path', default=None,
245 help='root directory for breakpad symbols')
246 parser.add_argument('--official_build', action='store_true', default=False,
247 help='point to official symbol server')
248 parser.add_argument('--regenerate', action='store_true', default=False,
249 help='regenerate all symbols')
250 parser.add_argument('--upload-count', type=int, default=None,
251 help='only upload # number of symbols')
252 parser.add_argument('--strip_cfi', type=int,
253 default=CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024),
254 help='strip CFI data for files above this size')
255 parser.add_argument('--testing', action='store_true', default=False,
256 help='run in testing mode')
257 parser.add_argument('--yes', action='store_true', default=False,
258 help='answer yes to all prompts')
259
260 opts = parser.parse_args(argv)
261
262 if opts.board is None:
263 cros_build_lib.Die('--board is required')
264
265 if opts.breakpad_root and opts.regenerate:
266 cros_build_lib.Die('--regenerate may not be used with --breakpad_root')
267
268 if opts.testing:
269 # TODO(build): Kill off --testing mode once unittests are up-to-snuff.
270 cros_build_lib.Info('running in testing mode')
271 # pylint: disable=W0601,W0603
272 global INITIAL_RETRY_DELAY, SymUpload, DEFAULT_SLEEP_DELAY
273 INITIAL_RETRY_DELAY = DEFAULT_SLEEP_DELAY = 0
274 SymUpload = TestingSymUpload
275
276 if not opts.yes:
277 query = textwrap.wrap(textwrap.dedent("""
278 Uploading symbols for an entire Chromium OS build is really only
279 necessary for release builds and in a few cases for developers
280 to debug problems. It will take considerable time to run. For
281 developer debugging purposes, consider instead passing specific
282 files to upload.
283 """), 80)
284 cros_build_lib.Warning('\n%s', '\n'.join(query))
285 if not cros_build_lib.BooleanPrompt(
286 prompt='Are you sure you want to upload all build symbols',
287 default=False):
288 cros_build_lib.Die('better safe than sorry')
289
290 ret = 0
291 if opts.regenerate:
292 ret += GenerateBreakpadSymbols(opts.board, breakpad_dir=opts.breakpad_root)
293
294 ret += UploadSymbols(opts.board, official=opts.official_build,
295 breakpad_dir=opts.breakpad_root,
296 file_limit=opts.strip_cfi, sleep=DEFAULT_SLEEP_DELAY,
297 upload_count=opts.upload_count)
298 if ret:
299 cros_build_lib.Error('encountered %i problem(s)', ret)
300 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
301 # return 0 in case we are a multiple of the mask.
302 ret = 1
303
304 return ret