Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 """Commit queue executable. | 5 """Commit queue executable. |
| 6 | 6 |
| 7 Reuse Rietveld and the Chromium Try Server to process and automatically commit | 7 Reuse Rietveld and the Chromium Try Server to process and automatically commit |
| 8 patches. | 8 patches. |
| 9 """ | 9 """ |
| 10 | 10 |
| 11 import logging | 11 import logging |
| 12 import logging.handlers | 12 import logging.handlers |
| 13 import optparse | 13 import optparse |
| 14 import os | 14 import os |
| 15 import signal | 15 import signal |
| 16 import sys | 16 import sys |
| 17 import tempfile | |
| 17 import time | 18 import time |
| 18 | 19 |
| 19 import find_depot_tools # pylint: disable=W0611 | 20 import find_depot_tools # pylint: disable=W0611 |
| 20 import checkout | 21 import checkout |
| 21 import fix_encoding | 22 import fix_encoding |
| 22 import rietveld | 23 import rietveld |
| 23 import subprocess2 | 24 import subprocess2 |
| 24 | 25 |
| 25 import async_push | 26 import async_push |
| 26 import cq_alerts | 27 import cq_alerts |
| (...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 116 logging_rotating_file = logging.handlers.RotatingFileHandler( | 117 logging_rotating_file = logging.handlers.RotatingFileHandler( |
| 117 filename=os.path.join(log_directory, 'commit_queue.log'), | 118 filename=os.path.join(log_directory, 'commit_queue.log'), |
| 118 maxBytes= 10*1024*1024, | 119 maxBytes= 10*1024*1024, |
| 119 backupCount=50) | 120 backupCount=50) |
| 120 logging_rotating_file.setLevel(logging.DEBUG) | 121 logging_rotating_file.setLevel(logging.DEBUG) |
| 121 logging_rotating_file.setFormatter(logging.Formatter( | 122 logging_rotating_file.setFormatter(logging.Formatter( |
| 122 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)4d): %(message)s')) | 123 '%(asctime)s %(levelname)-8s %(module)15s(%(lineno)4d): %(message)s')) |
| 123 logging.getLogger().addHandler(logging_rotating_file) | 124 logging.getLogger().addHandler(logging_rotating_file) |
| 124 | 125 |
| 125 | 126 |
| 127 class SignalInterrupt(Exception): | |
| 128 """Exception that indicates being interrupted by a caught signal.""" | |
| 129 | |
| 130 def __init__(self, signal_set=None, *args, **kwargs): | |
| 131 super(SignalInterrupt, self).__init__(*args, **kwargs) | |
| 132 self.signal_set = signal_set | |
| 133 | |
| 134 | |
| 135 def SaveDatabaseCopyForDebugging(db_path): | |
| 136 """Saves database file for debugging. Returns name of the saved file.""" | |
| 137 with tempfile.NamedTemporaryFile( | |
| 138 dir=os.path.dirname(db_path), | |
| 139 prefix='db.debug.', | |
| 140 suffix='.json', | |
| 141 delete=False) as tmp_file: | |
| 142 with open(db_path) as db_file: | |
| 143 tmp_file.write(db_file.read()) | |
|
iannucci
2013/09/25 22:15:49
use one of the shutil copy methods to avoid readin
Paweł Hajdan Jr.
2013/09/25 23:27:17
Done.
| |
| 144 return tmp_file.name | |
| 145 | |
| 146 | |
| 126 def main(): | 147 def main(): |
| 127 parser = optparse.OptionParser( | 148 parser = optparse.OptionParser( |
| 128 description=sys.modules['__main__'].__doc__) | 149 description=sys.modules['__main__'].__doc__) |
| 129 project_choices = projects.supported_projects() | 150 project_choices = projects.supported_projects() |
| 130 parser.add_option('-v', '--verbose', action='store_true') | 151 parser.add_option('-v', '--verbose', action='store_true') |
| 131 parser.add_option( | 152 parser.add_option( |
| 132 '--no-dry-run', | 153 '--no-dry-run', |
| 133 action='store_false', | 154 action='store_false', |
| 134 dest='dry_run', | 155 dest='dry_run', |
| 135 default=True, | 156 default=True, |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 228 # Disable the checkout. | 249 # Disable the checkout. |
| 229 print 'Using no checkout' | 250 print 'Using no checkout' |
| 230 pc.context.checkout = FakeCheckout() | 251 pc.context.checkout = FakeCheckout() |
| 231 else: | 252 else: |
| 232 print 'Using read-only checkout' | 253 print 'Using read-only checkout' |
| 233 pc.context.checkout = checkout.ReadOnlyCheckout(pc.context.checkout) | 254 pc.context.checkout = checkout.ReadOnlyCheckout(pc.context.checkout) |
| 234 # Save pushed events on disk. | 255 # Save pushed events on disk. |
| 235 print 'Using read-only chromium-status interface' | 256 print 'Using read-only chromium-status interface' |
| 236 pc.context.status = async_push.AsyncPushStore() | 257 pc.context.status = async_push.AsyncPushStore() |
| 237 | 258 |
| 259 landmine_path = os.path.join(work_dir, | |
| 260 pc.context.checkout.project_name + '.landmine') | |
| 238 db_path = os.path.join(work_dir, pc.context.checkout.project_name + '.json') | 261 db_path = os.path.join(work_dir, pc.context.checkout.project_name + '.json') |
| 239 if os.path.isfile(db_path): | 262 if os.path.isfile(db_path): |
| 240 try: | 263 if os.path.isfile(landmine_path): |
| 241 pc.load(db_path) | 264 debugging_path = SaveDatabaseCopyForDebugging(db_path) |
| 242 except ValueError: | |
| 243 os.remove(db_path) | 265 os.remove(db_path) |
| 266 logging.warning(('Deleting database because previous shutdown ' | |
| 267 'was unclean. The copy of the database is saved ' | |
| 268 'as %s.') % debugging_path) | |
| 269 else: | |
| 270 try: | |
| 271 pc.load(db_path) | |
| 272 except ValueError as e: | |
| 273 debugging_path = SaveDatabaseCopyForDebugging(db_path) | |
| 274 os.remove(db_path) | |
| 275 logging.warning(('Failed to parse database (%r), deleting it. ' | |
| 276 'The copy of the database is saved as %s.') % | |
| 277 (e, debugging_path)) | |
| 278 raise e | |
| 279 | |
| 280 # Create a file to indicate unclean shutdown. | |
| 281 with open(landmine_path, 'w'): | |
| 282 pass | |
| 244 | 283 |
| 245 sig_handler.installHandlers( | 284 sig_handler.installHandlers( |
| 246 signal.SIGINT, | 285 signal.SIGINT, |
| 247 signal.SIGHUP | 286 signal.SIGHUP |
| 248 ) | 287 ) |
| 249 | 288 |
| 250 # Sync every 5 minutes. | 289 # Sync every 5 minutes. |
| 251 SYNC_DELAY = 5*60 | 290 SYNC_DELAY = 5*60 |
| 252 try: | 291 try: |
| 253 if options.query_only: | 292 if options.query_only: |
| 254 pc.look_for_new_pending_commit() | 293 pc.look_for_new_pending_commit() |
| 255 pc.update_status() | 294 pc.update_status() |
| 256 print(str(pc.queue)) | 295 print(str(pc.queue)) |
| 296 os.remove(landmine_path) | |
| 257 return 0 | 297 return 0 |
| 258 | 298 |
| 259 now = time.time() | 299 now = time.time() |
| 260 next_loop = now + options.poll_interval | 300 next_loop = now + options.poll_interval |
| 261 # First sync is on second loop. | 301 # First sync is on second loop. |
| 262 next_sync = now + options.poll_interval * 2 | 302 next_sync = now + options.poll_interval * 2 |
| 263 while True: | 303 while True: |
| 264 # In theory, we would gain in performance to parallelize these tasks. In | 304 # In theory, we would gain in performance to parallelize these tasks. In |
| 265 # practice I'm not sure it matters. | 305 # practice I'm not sure it matters. |
| 266 pc.look_for_new_pending_commit() | 306 pc.look_for_new_pending_commit() |
| 267 pc.process_new_pending_commit() | 307 pc.process_new_pending_commit() |
| 268 pc.update_status() | 308 pc.update_status() |
| 269 pc.scan_results() | 309 pc.scan_results() |
| 270 if sig_handler.getTriggeredSignals(): | 310 if sig_handler.getTriggeredSignals(): |
| 271 raise KeyboardInterrupt() | 311 raise SignalInterrupt(signal_set=sig_handler.getTriggeredSignals()) |
| 272 # Save the db at each loop. The db can easily be in the 1mb range so | 312 # Save the db at each loop. The db can easily be in the 1mb range so |
| 273 # it's slowing down the CQ a tad but it in the 100ms range even for that | 313 # it's slowing down the CQ a tad but it in the 100ms range even for that |
| 274 # size. | 314 # size. |
| 275 pc.save(db_path) | 315 pc.save(db_path) |
| 276 | 316 |
| 277 # More than a second to wait and due to sync. | 317 # More than a second to wait and due to sync. |
| 278 now = time.time() | 318 now = time.time() |
| 279 if (next_loop - now) >= 1 and (next_sync - now) <= 0: | 319 if (next_loop - now) >= 1 and (next_sync - now) <= 0: |
| 280 if sys.stdout.isatty(): | 320 if sys.stdout.isatty(): |
| 281 sys.stdout.write('Syncing while waiting \r') | 321 sys.stdout.write('Syncing while waiting \r') |
| 282 sys.stdout.flush() | 322 sys.stdout.flush() |
| 283 try: | 323 try: |
| 284 pc.context.checkout.prepare(None) | 324 pc.context.checkout.prepare(None) |
| 285 except subprocess2.CalledProcessError as e: | 325 except subprocess2.CalledProcessError as e: |
| 286 # Don't crash, most of the time it's the svn server that is dead. | 326 # Don't crash, most of the time it's the svn server that is dead. |
| 287 # How fun. Send a stack trace to annoy the maintainer. | 327 # How fun. Send a stack trace to annoy the maintainer. |
| 288 errors.send_stack(e) | 328 errors.send_stack(e) |
| 289 next_sync = time.time() + SYNC_DELAY | 329 next_sync = time.time() + SYNC_DELAY |
| 290 | 330 |
| 291 now = time.time() | 331 now = time.time() |
| 292 next_loop = max(now, next_loop) | 332 next_loop = max(now, next_loop) |
| 293 while True: | 333 while True: |
| 294 # Abort if any signals are set | 334 # Abort if any signals are set |
| 295 if sig_handler.getTriggeredSignals(): | 335 if sig_handler.getTriggeredSignals(): |
| 296 raise KeyboardInterrupt() | 336 raise SignalInterrupt(signal_set=sig_handler.getTriggeredSignals()) |
| 297 delay = next_loop - now | 337 delay = next_loop - now |
| 298 if delay <= 0: | 338 if delay <= 0: |
| 299 break | 339 break |
| 300 if sys.stdout.isatty(): | 340 if sys.stdout.isatty(): |
| 301 sys.stdout.write('Sleeping for %1.1f seconds \r' % delay) | 341 sys.stdout.write('Sleeping for %1.1f seconds \r' % delay) |
| 302 sys.stdout.flush() | 342 sys.stdout.flush() |
| 303 time.sleep(min(delay, 0.1)) | 343 time.sleep(min(delay, 0.1)) |
| 304 now = time.time() | 344 now = time.time() |
| 305 if sys.stdout.isatty(): | 345 if sys.stdout.isatty(): |
| 306 sys.stdout.write('Running (please do not interrupt) \r') | 346 sys.stdout.write('Running (please do not interrupt) \r') |
| 307 sys.stdout.flush() | 347 sys.stdout.flush() |
| 308 next_loop = time.time() + options.poll_interval | 348 next_loop = time.time() + options.poll_interval |
| 309 except: # Catch all fatal exit conditions. | 349 except: # Catch all fatal exit conditions. |
| 310 logging.exception('CQ loop terminating') | 350 logging.exception('CQ loop terminating') |
| 311 raise | 351 raise |
| 312 finally: | 352 finally: |
| 313 logging.warning('Saving db...') | 353 logging.warning('Saving db...') |
| 314 pc.save(db_path) | 354 pc.save(db_path) |
| 315 pc.close() | 355 pc.close() |
| 316 logging.warning('db save successful.') | 356 logging.warning('db save successful.') |
| 317 except KeyboardInterrupt as e: | 357 except SignalInterrupt: |
| 318 print 'Bye bye' | 358 # This is considered a clean shutdown: we only throw this exception |
| 359 # from selected places in the code where the database should be | |
| 360 # in a known and consistent state. | |
| 361 os.remove(landmine_path) | |
| 362 | |
| 363 print 'By bye (SignalInterrupt)' | |
|
iannucci
2013/09/25 22:15:49
'Bye bye'
Bye -> abbreviation of 'goodbye'
By ->
Paweł Hajdan Jr.
2013/09/25 23:27:17
Done. Actually this was accidental deletion in vim
| |
| 364 # 23 is an arbitrary value to signal loop.sh that it must stop looping. | |
| 365 return 23 | |
| 366 except KeyboardInterrupt: | |
| 367 # This is actually an unclean shutdown. Do not remove the landmine file. | |
| 368 # One example of this is user hitting ctrl-c twice at an arbitrary point | |
| 369 # inside the CQ loop. There are no guarantees about consistent state | |
| 370 # of the database then. | |
| 371 | |
| 372 print 'Bye bye (KeyboardInterrupt - this is considered unclean shutdown)' | |
| 319 # 23 is an arbitrary value to signal loop.sh that it must stop looping. | 373 # 23 is an arbitrary value to signal loop.sh that it must stop looping. |
| 320 return 23 | 374 return 23 |
| 321 except errors.ConfigurationError as e: | 375 except errors.ConfigurationError as e: |
| 322 parser.error(str(e)) | 376 parser.error(str(e)) |
| 323 return 1 | 377 return 1 |
| 378 | |
| 379 # CQ generally doesn't exit by itself, but if we ever get here, it looks | |
| 380 # like a clean shutdown so remove the landmine file. | |
| 381 os.remove(landmine_path) | |
| 324 return 0 | 382 return 0 |
| 325 | 383 |
| 326 | 384 |
| 327 if __name__ == '__main__': | 385 if __name__ == '__main__': |
| 328 fix_encoding.fix_encoding() | 386 fix_encoding.fix_encoding() |
| 329 sys.exit(main()) | 387 sys.exit(main()) |
| OLD | NEW |