blob: 5f8e956f42f090416b4d8fd0268a90b5faf4f0ff [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
155def UploadSymbols(board, official=False, breakpad_dir=None,
156 file_limit=DEFAULT_FILE_LIMIT, sleep=DEFAULT_SLEEP_DELAY,
157 upload_count=None):
158 """Upload all the generated symbols for |board| to the crash server
159
160 Args:
161 board: The board whose symbols we wish to upload
162 official: Use the official symbol server rather than the staging one
163 breakpad_dir: The full path to the breakpad directory where symbols live
164 file_limit: The max file size of a symbol file before we try to strip it
165 sleep: How long to sleep in between uploads
166 upload_count: If set, only upload this many symbols (meant for testing)
167 Returns:
168 False if some errors were encountered, True otherwise.
169 """
170 num_errors = 0
171
172 if official:
173 upload_url = OFFICIAL_UPLOAD_URL
174 else:
175 cros_build_lib.Warning('unofficial builds upload to the staging server')
176 upload_url = STAGING_UPLOAD_URL
177
178 if breakpad_dir is None:
179 breakpad_dir = FindBreakpadDir(board)
180 cros_build_lib.Info('uploading symbols to %s from %s', upload_url,
181 breakpad_dir)
182
183 cros_build_lib.Info('uploading all breakpad symbol files')
184 # We need to limit ourselves to one upload at a time to avoid the server
185 # kicking in DoS protection. See these bugs for more details:
186 # http://crbug.com/209442
187 # http://crbug.com/212496
188 bg_errors = multiprocessing.Value('i')
189 with parallel.BackgroundTaskRunner(UploadSymbol, file_limit=file_limit,
190 sleep=sleep, num_errors=bg_errors,
191 processes=1) as queue:
192 for root, _, files in os.walk(breakpad_dir):
193 if upload_count == 0:
194 break
195
196 for sym_file in files:
197 if sym_file.endswith('.sym'):
198 sym_file = os.path.join(root, sym_file)
199 queue.put([sym_file, upload_url])
200
201 if upload_count is not None:
202 upload_count -= 1
203 if upload_count == 0:
204 break
205 num_errors += bg_errors.value
206
207 return num_errors
208
209
210def GenerateBreakpadSymbols(board, breakpad_dir=None):
211 """Generate all the symbols for this board
212
213 Note: this should be merged with buildbot_commands.GenerateBreakpadSymbols()
214 once we rewrite cros_generate_breakpad_symbols in python.
215
216 Args:
217 board: The board whose symbols we wish to generate
218 breakpad_dir: The full path to the breakpad directory where symbols live
219 """
220 if breakpad_dir is None:
221 breakpad_dir = FindBreakpadDir(board)
222
223 cros_build_lib.Info('clearing out %s', breakpad_dir)
224 osutils.RmDir(breakpad_dir, ignore_missing=True, sudo=True)
225
226 cros_build_lib.Info('generating all breakpad symbol files')
227 cmd = [os.path.join(constants.CROSUTILS_DIR,
228 'cros_generate_breakpad_symbols'),
229 '--board', board]
230 if cros_build_lib.logger.getEffectiveLevel() < logging.INFO:
231 cmd += ['--verbose']
232 result = cros_build_lib.RunCommand(cmd, error_code_ok=True)
233 if result.returncode:
234 cros_build_lib.Warning('errors hit while generating symbols; '
235 'uploading anyways')
236 return 1
237
238 return 0
239
240
241def FindBreakpadDir(board):
242 """Given a |board|, return the path to the breakpad dir for it"""
243 return os.path.join('/build', board, 'usr', 'lib', 'debug', 'breakpad')
244
245
246def main(argv):
247 parser = commandline.ArgumentParser(description=__doc__)
248
249 parser.add_argument('--board', default=None,
250 help='board to build packages for')
251 parser.add_argument('--breakpad_root', type='path', default=None,
252 help='root directory for breakpad symbols')
253 parser.add_argument('--official_build', action='store_true', default=False,
254 help='point to official symbol server')
255 parser.add_argument('--regenerate', action='store_true', default=False,
256 help='regenerate all symbols')
257 parser.add_argument('--upload-count', type=int, default=None,
258 help='only upload # number of symbols')
259 parser.add_argument('--strip_cfi', type=int,
260 default=CRASH_SERVER_FILE_LIMIT - (10 * 1024 * 1024),
261 help='strip CFI data for files above this size')
262 parser.add_argument('--testing', action='store_true', default=False,
263 help='run in testing mode')
264 parser.add_argument('--yes', action='store_true', default=False,
265 help='answer yes to all prompts')
266
267 opts = parser.parse_args(argv)
268
269 if opts.board is None:
270 cros_build_lib.Die('--board is required')
271
272 if opts.breakpad_root and opts.regenerate:
273 cros_build_lib.Die('--regenerate may not be used with --breakpad_root')
274
275 if opts.testing:
276 # TODO(build): Kill off --testing mode once unittests are up-to-snuff.
277 cros_build_lib.Info('running in testing mode')
278 # pylint: disable=W0601,W0603
279 global INITIAL_RETRY_DELAY, SymUpload, DEFAULT_SLEEP_DELAY
280 INITIAL_RETRY_DELAY = DEFAULT_SLEEP_DELAY = 0
281 SymUpload = TestingSymUpload
282
283 if not opts.yes:
284 query = textwrap.wrap(textwrap.dedent("""
285 Uploading symbols for an entire Chromium OS build is really only
286 necessary for release builds and in a few cases for developers
287 to debug problems. It will take considerable time to run. For
288 developer debugging purposes, consider instead passing specific
289 files to upload.
290 """), 80)
291 cros_build_lib.Warning('\n%s', '\n'.join(query))
292 if not cros_build_lib.BooleanPrompt(
293 prompt='Are you sure you want to upload all build symbols',
294 default=False):
295 cros_build_lib.Die('better safe than sorry')
296
297 ret = 0
298 if opts.regenerate:
299 ret += GenerateBreakpadSymbols(opts.board, breakpad_dir=opts.breakpad_root)
300
301 ret += UploadSymbols(opts.board, official=opts.official_build,
302 breakpad_dir=opts.breakpad_root,
303 file_limit=opts.strip_cfi, sleep=DEFAULT_SLEEP_DELAY,
304 upload_count=opts.upload_count)
305 if ret:
306 cros_build_lib.Error('encountered %i problem(s)', ret)
307 # Since exit(status) gets masked, clamp it to 1 so we don't inadvertently
308 # return 0 in case we are a multiple of the mask.
309 ret = 1
310
311 return ret