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