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