Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(186)

Side by Side Diff: appengine/swarming/swarming_bot/bot_code/bot_main.py

Issue 2024313003: Send authorization headers when calling Swarming backend. (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@master
Patch Set: rebase Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright 2013 The LUCI Authors. All rights reserved. 1 # Copyright 2013 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 """Swarming bot main process. 5 """Swarming bot main process.
6 6
7 This is the program that communicates with the Swarming server, ensures the code 7 This is the program that communicates with the Swarming server, ensures the code
8 is always up to date and executes a child process to run tasks and upload 8 is always up to date and executes a child process to run tasks and upload
9 results back. 9 results back.
10 10
11 It manages self-update and rebooting the host in case of problems. 11 It manages self-update and rebooting the host in case of problems.
12 12
13 Set the environment variable SWARMING_LOAD_TEST=1 to disable the use of 13 Set the environment variable SWARMING_LOAD_TEST=1 to disable the use of
14 server-provided bot_config.py. This permits safe load testing. 14 server-provided bot_config.py. This permits safe load testing.
15 """ 15 """
16 16
17 import contextlib 17 import contextlib
18 import json 18 import json
19 import logging 19 import logging
20 import optparse 20 import optparse
21 import os 21 import os
22 import shutil 22 import shutil
23 import sys 23 import sys
24 import tempfile 24 import tempfile
25 import threading 25 import threading
26 import time 26 import time
27 import traceback 27 import traceback
28 import zipfile 28 import zipfile
29 29
30 import bot_auth
30 import common 31 import common
32 import file_refresher
33 import remote_client
31 import singleton 34 import singleton
32 from api import bot 35 from api import bot
33 from api import os_utilities 36 from api import os_utilities
34 from utils import file_path 37 from utils import file_path
35 from utils import net 38 from utils import net
36 from utils import on_error 39 from utils import on_error
37 from utils import subprocess42 40 from utils import subprocess42
38 from utils import zip_package 41 from utils import zip_package
39 42
40 43
(...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after
170 should_continue = bot_config.setup_bot(botobj) 173 should_continue = bot_config.setup_bot(botobj)
171 except Exception as e: 174 except Exception as e:
172 msg = '%s\n%s' % (e, traceback.format_exc()[-2048:]) 175 msg = '%s\n%s' % (e, traceback.format_exc()[-2048:])
173 botobj.post_error('bot_config.setup_bot() threw: %s' % msg) 176 botobj.post_error('bot_config.setup_bot() threw: %s' % msg)
174 return 177 return
175 178
176 if not should_continue and not skip_reboot: 179 if not should_continue and not skip_reboot:
177 botobj.restart('Starting new swarming bot: %s' % THIS_FILE) 180 botobj.restart('Starting new swarming bot: %s' % THIS_FILE)
178 181
179 182
183 def get_authentication_headers(botobj):
184 """Calls bot_config.get_authentication_headers() if it is defined.
185
186 Doesn't catch exceptions.
187 """
188 if _in_load_test_mode():
189 return (None, None)
190 logging.info('get_authentication_headers()')
191 from config import bot_config
192 func = getattr(bot_config, 'get_authentication_headers', None)
193 return func(botobj) if func else (None, None)
194
195
180 ### end of bot_config handler part. 196 ### end of bot_config handler part.
181 197
182 198
183 def get_min_free_space(): 199 def get_min_free_space():
184 """Returns free disk space needed. 200 """Returns free disk space needed.
185 201
186 Add a "250 MiB slack space" for logs, temporary files and whatever other leak. 202 Add a "250 MiB slack space" for logs, temporary files and whatever other leak.
187 """ 203 """
188 return int((os_utilities.get_min_free_space(THIS_FILE) + 250.) * 1024 * 1024) 204 return int((os_utilities.get_min_free_space(THIS_FILE) + 250.) * 1024 * 1024)
189 205
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
228 exception handler for incoming commands from the Swarming server. If for 244 exception handler for incoming commands from the Swarming server. If for
229 any reason the local test runner script can not be run successfully, 245 any reason the local test runner script can not be run successfully,
230 this function is invoked. 246 this function is invoked.
231 """ 247 """
232 logging.error('Error: %s', error) 248 logging.error('Error: %s', error)
233 data = { 249 data = {
234 'id': botobj.id, 250 'id': botobj.id,
235 'message': error, 251 'message': error,
236 'task_id': task_id, 252 'task_id': task_id,
237 } 253 }
238 return net.url_read_json( 254 return botobj.remote.url_read_json(
239 botobj.server + '/swarming/api/v1/bot/task_error/%s' % task_id, data=data) 255 '/swarming/api/v1/bot/task_error/%s' % task_id, data=data)
240 256
241 257
242 def on_shutdown_hook(b): 258 def on_shutdown_hook(b):
243 """Called when the bot is restarting.""" 259 """Called when the bot is restarting."""
244 call_hook(b, 'on_bot_shutdown') 260 call_hook(b, 'on_bot_shutdown')
245 # Aggressively set itself up so we ensure the auto-reboot configuration is 261 # Aggressively set itself up so we ensure the auto-reboot configuration is
246 # fine before restarting the host. This is important as some tasks delete the 262 # fine before restarting the host. This is important as some tasks delete the
247 # autorestart script (!) 263 # autorestart script (!)
248 setup_bot(True) 264 setup_bot(True)
249 265
250 266
251 def get_bot(): 267 def get_bot():
252 """Returns a valid Bot instance. 268 """Returns a valid Bot instance.
253 269
254 Should only be called once in the process lifetime. 270 Should only be called once in the process lifetime.
255 """ 271 """
256 # This variable is used to bootstrap the initial bot.Bot object, which then is 272 # This variable is used to bootstrap the initial bot.Bot object, which then is
257 # used to get the dimensions and state. 273 # used to get the dimensions and state.
258 attributes = { 274 attributes = {
259 'dimensions': {u'id': ['none']}, 275 'dimensions': {u'id': ['none']},
260 'state': {}, 276 'state': {},
261 'version': generate_version(), 277 'version': generate_version(),
262 } 278 }
263 config = get_config() 279 config = get_config()
264 assert not config['server'].endswith('/'), config 280 assert not config['server'].endswith('/'), config
265 281
266 # Create a temporary object to call the hooks. 282 # Use temporary Bot object to call get_attributes. Attributes are needed to
283 # construct the "real" bot.Bot.
284 attributes = get_attributes(
285 bot.Bot(
286 remote_client.RemoteClient(config['server'], None),
287 attributes,
288 config['server'],
289 config['server_version'],
290 os.path.dirname(THIS_FILE),
291 on_shutdown_hook))
292
293 # Make remote client callback use the returned bot object. We assume here
294 # RemoteClient doesn't call its callback in the constructor (since 'botobj' is
295 # undefined during the construction).
267 botobj = bot.Bot( 296 botobj = bot.Bot(
297 remote_client.RemoteClient(
298 config['server'],
299 lambda: get_authentication_headers(botobj)),
268 attributes, 300 attributes,
269 config['server'], 301 config['server'],
270 config['server_version'], 302 config['server_version'],
271 os.path.dirname(THIS_FILE), 303 os.path.dirname(THIS_FILE),
272 on_shutdown_hook) 304 on_shutdown_hook)
273 return bot.Bot( 305 return botobj
274 get_attributes(botobj),
275 config['server'],
276 config['server_version'],
277 os.path.dirname(THIS_FILE),
278 on_shutdown_hook)
279 306
280 307
281 def clean_isolated_cache(botobj): 308 def clean_isolated_cache(botobj):
282 """Asks run_isolated to clean its cache. 309 """Asks run_isolated to clean its cache.
283 310
284 This may take a while but it ensures that in the case of a run_isolated run 311 This may take a while but it ensures that in the case of a run_isolated run
285 failed and it temporarily used more space than min_free_disk, it can cleans up 312 failed and it temporarily used more space than min_free_disk, it can cleans up
286 the mess properly. 313 the mess properly.
287 314
288 It will remove unexpected files, remove corrupted files, trim the cache size 315 It will remove unexpected files, remove corrupted files, trim the cache size
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
335 # First thing is to get an arbitrary url. This also ensures the network is 362 # First thing is to get an arbitrary url. This also ensures the network is
336 # up and running, which is necessary before trying to get the FQDN below. 363 # up and running, which is necessary before trying to get the FQDN below.
337 resp = net.url_read(config['server'] + '/swarming/api/v1/bot/server_ping') 364 resp = net.url_read(config['server'] + '/swarming/api/v1/bot/server_ping')
338 if resp is None: 365 if resp is None:
339 logging.error('No response from server_ping') 366 logging.error('No response from server_ping')
340 except Exception as e: 367 except Exception as e:
341 # url_read() already traps pretty much every exceptions. This except 368 # url_read() already traps pretty much every exceptions. This except
342 # clause is kept there "just in case". 369 # clause is kept there "just in case".
343 logging.exception('server_ping threw') 370 logging.exception('server_ping threw')
344 371
372 # Next we make sure the bot can make authenticated calls by grabbing
373 # the auth headers, retrying on errors a bunch of times. We don't give up
374 # if it fails though (maybe the bot will "fix itself" later).
375 botobj = get_bot()
376 try:
377 botobj.remote.initialize(quit_bit)
378 except remote_client.InitializationError as exc:
379 botobj.post_error('failed to grab auth headers: %s' % exc.last_error)
380 logging.error('Can\'t grab auth headers, continuing anyway...')
381
345 if quit_bit.is_set(): 382 if quit_bit.is_set():
346 logging.info('Early quit 1') 383 logging.info('Early quit 1')
347 return 0 384 return 0
348 385
349 # If this fails, there's hardly anything that can be done, the bot can't 386 # If this fails, there's hardly anything that can be done, the bot can't
350 # even get to the point to be able to self-update. 387 # even get to the point to be able to self-update.
351 botobj = get_bot() 388 resp = botobj.remote.url_read_json(
352 resp = net.url_read_json( 389 '/swarming/api/v1/bot/handshake', data=botobj._attributes)
353 botobj.server + '/swarming/api/v1/bot/handshake',
354 data=botobj._attributes)
355 if not resp: 390 if not resp:
356 logging.error('Failed to contact for handshake') 391 logging.error('Failed to contact for handshake')
357 else: 392 else:
358 logging.info('Connected to %s', resp.get('server_version')) 393 logging.info('Connected to %s', resp.get('server_version'))
359 if resp.get('bot_version') != botobj._attributes['version']: 394 if resp.get('bot_version') != botobj._attributes['version']:
360 logging.warning( 395 logging.warning(
361 'Found out we\'ll need to update: server said %s; we\'re %s', 396 'Found out we\'ll need to update: server said %s; we\'re %s',
362 resp.get('bot_version'), botobj._attributes['version']) 397 resp.get('bot_version'), botobj._attributes['version'])
363 398
364 if arg_error: 399 if arg_error:
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
411 return 0 446 return 0
412 447
413 448
414 def poll_server(botobj, quit_bit): 449 def poll_server(botobj, quit_bit):
415 """Polls the server to run one loop. 450 """Polls the server to run one loop.
416 451
417 Returns True if executed some action, False if server asked the bot to sleep. 452 Returns True if executed some action, False if server asked the bot to sleep.
418 """ 453 """
419 # Access to a protected member _XXX of a client class - pylint: disable=W0212 454 # Access to a protected member _XXX of a client class - pylint: disable=W0212
420 start = time.time() 455 start = time.time()
421 resp = net.url_read_json( 456 resp = botobj.remote.url_read_json(
422 botobj.server + '/swarming/api/v1/bot/poll', data=botobj._attributes) 457 '/swarming/api/v1/bot/poll', data=botobj._attributes)
423 if not resp: 458 if not resp:
424 return False 459 return False
425 logging.debug('Server response:\n%s', resp) 460 logging.debug('Server response:\n%s', resp)
426 461
427 cmd = resp['cmd'] 462 cmd = resp['cmd']
428 if cmd == 'sleep': 463 if cmd == 'sleep':
429 quit_bit.wait(resp['duration']) 464 quit_bit.wait(resp['duration'])
430 return False 465 return False
431 466
432 if cmd == 'terminate': 467 if cmd == 'terminate':
433 quit_bit.set() 468 quit_bit.set()
434 # This is similar to post_update() in task_runner.py. 469 # This is similar to post_update() in task_runner.py.
435 params = { 470 params = {
436 'cost_usd': 0, 471 'cost_usd': 0,
437 'duration': 0, 472 'duration': 0,
438 'exit_code': 0, 473 'exit_code': 0,
439 'hard_timeout': False, 474 'hard_timeout': False,
440 'id': botobj.id, 475 'id': botobj.id,
441 'io_timeout': False, 476 'io_timeout': False,
442 'output': '', 477 'output': '',
443 'output_chunk_start': 0, 478 'output_chunk_start': 0,
444 'task_id': resp['task_id'], 479 'task_id': resp['task_id'],
445 } 480 }
446 net.url_read_json( 481 botobj.remote.url_read_json(
447 botobj.server + '/swarming/api/v1/bot/task_update/%s' % resp['task_id'], 482 '/swarming/api/v1/bot/task_update/%s' % resp['task_id'],
448 data=params) 483 data=params)
449 return False 484 return False
450 485
451 if cmd == 'run': 486 if cmd == 'run':
452 if run_manifest(botobj, resp['manifest'], start): 487 if run_manifest(botobj, resp['manifest'], start):
453 # Completed a task successfully so update swarming_bot.zip if necessary. 488 # Completed a task successfully so update swarming_bot.zip if necessary.
454 update_lkgbc(botobj) 489 update_lkgbc(botobj)
455 # Clean up cache after a task 490 # Clean up cache after a task
456 clean_isolated_cache(botobj) 491 clean_isolated_cache(botobj)
457 # TODO(maruel): Handle the case where quit_bit.is_set() happens here. This 492 # TODO(maruel): Handle the case where quit_bit.is_set() happens here. This
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after
496 if not manifest['command']: 531 if not manifest['command']:
497 hard_timeout += manifest['io_timeout'] or 600 532 hard_timeout += manifest['io_timeout'] or 600
498 533
499 url = manifest.get('host', botobj.server) 534 url = manifest.get('host', botobj.server)
500 task_dimensions = manifest['dimensions'] 535 task_dimensions = manifest['dimensions']
501 task_result = {} 536 task_result = {}
502 537
503 failure = False 538 failure = False
504 internal_failure = False 539 internal_failure = False
505 msg = None 540 msg = None
541 auth_params_dumper = None
506 work_dir = os.path.join(botobj.base_dir, 'work') 542 work_dir = os.path.join(botobj.base_dir, 'work')
507 try: 543 try:
508 try: 544 try:
509 if os.path.isdir(work_dir): 545 if os.path.isdir(work_dir):
510 file_path.rmtree(work_dir) 546 file_path.rmtree(work_dir)
511 except OSError: 547 except OSError:
512 # If a previous task created an undeleteable file/directory inside 'work', 548 # If a previous task created an undeleteable file/directory inside 'work',
513 # make sure that following tasks are not affected. This is done by working 549 # make sure that following tasks are not affected. This is done by working
514 # around the undeleteable directory by creating a temporary directory 550 # around the undeleteable directory by creating a temporary directory
515 # instead. This is not normal behavior. The bot will report a failure on 551 # instead. This is not normal behavior. The bot will report a failure on
516 # start. 552 # start.
517 work_dir = tempfile.mkdtemp(dir=botobj.base_dir, prefix='work') 553 work_dir = tempfile.mkdtemp(dir=botobj.base_dir, prefix='work')
518 else: 554 else:
519 os.makedirs(work_dir) 555 os.makedirs(work_dir)
520 556
521 env = os.environ.copy() 557 env = os.environ.copy()
522 # Windows in particular does not tolerate unicode strings in environment 558 # Windows in particular does not tolerate unicode strings in environment
523 # variables. 559 # variables.
524 env['SWARMING_TASK_ID'] = task_id.encode('ascii') 560 env['SWARMING_TASK_ID'] = task_id.encode('ascii')
525 561
526 task_in_file = os.path.join(work_dir, 'task_runner_in.json') 562 task_in_file = os.path.join(work_dir, 'task_runner_in.json')
527 with open(task_in_file, 'wb') as f: 563 with open(task_in_file, 'wb') as f:
528 f.write(json.dumps(manifest)) 564 f.write(json.dumps(manifest))
529 call_hook(botobj, 'on_before_task') 565 call_hook(botobj, 'on_before_task')
530 task_result_file = os.path.join(work_dir, 'task_runner_out.json') 566 task_result_file = os.path.join(work_dir, 'task_runner_out.json')
531 if os.path.exists(task_result_file): 567 if os.path.exists(task_result_file):
532 os.remove(task_result_file) 568 os.remove(task_result_file)
569
570 # Start a thread that periodically puts authentication headers and other
571 # authentication related information to a file on disk. task_runner and its
572 # subprocesses read it from there before making authenticated HTTP calls.
573 auth_params_file = os.path.join(work_dir, 'bot_auth_params.json')
574 if botobj.remote.uses_auth:
575 env['SWARMING_AUTH_PARAMS'] = str(auth_params_file)
576 auth_params_dumper = file_refresher.FileRefresherThread(
577 auth_params_file, lambda: bot_auth.prepare_auth_params_json(botobj))
578 auth_params_dumper.start()
579 else:
580 env.pop('SWARMING_AUTH_PARAMS', None)
581 if os.path.exists(auth_params_file):
582 os.remove(auth_params_file)
583
533 command = [ 584 command = [
534 sys.executable, THIS_FILE, 'task_runner', 585 sys.executable, THIS_FILE, 'task_runner',
535 '--swarming-server', url, 586 '--swarming-server', url,
536 '--in-file', task_in_file, 587 '--in-file', task_in_file,
537 '--out-file', task_result_file, 588 '--out-file', task_result_file,
538 '--cost-usd-hour', str(botobj.state.get('cost_usd_hour') or 0.), 589 '--cost-usd-hour', str(botobj.state.get('cost_usd_hour') or 0.),
539 # Include the time taken to poll the task in the cost. 590 # Include the time taken to poll the task in the cost.
540 '--start', str(start), 591 '--start', str(start),
541 '--min-free-space', str(get_min_free_space()), 592 '--min-free-space', str(get_min_free_space()),
542 ] 593 ]
543 logging.debug('Running command: %s', command) 594 logging.debug('Running command: %s', command)
595
544 # Put the output file into the current working directory, which should be 596 # Put the output file into the current working directory, which should be
545 # the one containing swarming_bot.zip. 597 # the one containing swarming_bot.zip.
546 log_path = os.path.join(botobj.base_dir, 'logs', 'task_runner_stdout.log') 598 log_path = os.path.join(botobj.base_dir, 'logs', 'task_runner_stdout.log')
547 os_utilities.roll_log(log_path) 599 os_utilities.roll_log(log_path)
548 os_utilities.trim_rolled_log(log_path) 600 os_utilities.trim_rolled_log(log_path)
549 with open(log_path, 'a+b') as f: 601 with open(log_path, 'a+b') as f:
550 proc = subprocess42.Popen( 602 proc = subprocess42.Popen(
551 command, 603 command,
552 detached=True, 604 detached=True,
553 cwd=botobj.base_dir, 605 cwd=botobj.base_dir,
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
594 failure = bool(task_result.get('exit_code')) if task_result else False 646 failure = bool(task_result.get('exit_code')) if task_result else False
595 return not internal_failure and not failure 647 return not internal_failure and not failure
596 except Exception as e: 648 except Exception as e:
597 # Failures include IOError when writing if the disk is full, OSError if 649 # Failures include IOError when writing if the disk is full, OSError if
598 # swarming_bot.zip doesn't exist anymore, etc. 650 # swarming_bot.zip doesn't exist anymore, etc.
599 logging.exception('run_manifest failed') 651 logging.exception('run_manifest failed')
600 msg = 'Internal exception occured: %s\n%s' % ( 652 msg = 'Internal exception occured: %s\n%s' % (
601 e, traceback.format_exc()[-2048:]) 653 e, traceback.format_exc()[-2048:])
602 internal_failure = True 654 internal_failure = True
603 finally: 655 finally:
656 if auth_params_dumper:
657 auth_params_dumper.stop()
604 if internal_failure: 658 if internal_failure:
605 post_error_task(botobj, msg, task_id) 659 post_error_task(botobj, msg, task_id)
606 call_hook( 660 call_hook(
607 botobj, 'on_after_task', failure, internal_failure, task_dimensions, 661 botobj, 'on_after_task', failure, internal_failure, task_dimensions,
608 task_result) 662 task_result)
609 if os.path.isdir(work_dir): 663 if os.path.isdir(work_dir):
610 try: 664 try:
611 file_path.rmtree(work_dir) 665 file_path.rmtree(work_dir)
612 except Exception as e: 666 except Exception as e:
613 botobj.post_error( 667 botobj.post_error(
(...skipping 10 matching lines...) Expand all
624 678
625 Does not return. 679 Does not return.
626 """ 680 """
627 # Alternate between .1.zip and .2.zip. 681 # Alternate between .1.zip and .2.zip.
628 new_zip = 'swarming_bot.1.zip' 682 new_zip = 'swarming_bot.1.zip'
629 if os.path.basename(THIS_FILE) == new_zip: 683 if os.path.basename(THIS_FILE) == new_zip:
630 new_zip = 'swarming_bot.2.zip' 684 new_zip = 'swarming_bot.2.zip'
631 new_zip = os.path.join(os.path.dirname(THIS_FILE), new_zip) 685 new_zip = os.path.join(os.path.dirname(THIS_FILE), new_zip)
632 686
633 # Download as a new file. 687 # Download as a new file.
634 url = botobj.server + '/swarming/api/v1/bot/bot_code/%s' % version 688 url_path = '/swarming/api/v1/bot/bot_code/%s' % version
635 if not net.url_retrieve(new_zip, url): 689 if not botobj.remote.url_retrieve(new_zip, url_path):
636 # It can happen when a server is rapidly updated multiple times in a row. 690 # It can happen when a server is rapidly updated multiple times in a row.
637 botobj.post_error( 691 botobj.post_error(
638 'Unable to download %s from %s; first tried version %s' % 692 'Unable to download %s from %s; first tried version %s' %
639 (new_zip, url, version)) 693 (new_zip, botobj.server + url_path, version))
640 # Poll again, this may work next time. To prevent busy-loop, sleep a little. 694 # Poll again, this may work next time. To prevent busy-loop, sleep a little.
641 time.sleep(2) 695 time.sleep(2)
642 return 696 return
643 697
644 s = os.stat(new_zip) 698 s = os.stat(new_zip)
645 logging.info('Restarting to %s; %d bytes.', new_zip, s.st_size) 699 logging.info('Restarting to %s; %d bytes.', new_zip, s.st_size)
646 sys.stdout.flush() 700 sys.stdout.flush()
647 sys.stderr.flush() 701 sys.stderr.flush()
648 702
649 proc = subprocess42.Popen( 703 proc = subprocess42.Popen(
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after
744 os.path.dirname(THIS_FILE), 'logs', 'bot_std%s.log' % t) 798 os.path.dirname(THIS_FILE), 'logs', 'bot_std%s.log' % t)
745 os_utilities.roll_log(log_path) 799 os_utilities.roll_log(log_path)
746 os_utilities.trim_rolled_log(log_path) 800 os_utilities.trim_rolled_log(log_path)
747 801
748 error = None 802 error = None
749 if len(args) != 0: 803 if len(args) != 0:
750 error = 'Unexpected arguments: %s' % args 804 error = 'Unexpected arguments: %s' % args
751 try: 805 try:
752 return run_bot(error) 806 return run_bot(error)
753 finally: 807 finally:
754 call_hook(bot.Bot(None, None, None, os.path.dirname(THIS_FILE), None), 808 call_hook(bot.Bot(None, None, None, None, os.path.dirname(THIS_FILE), None),
755 'on_bot_shutdown') 809 'on_bot_shutdown')
756 logging.info('main() returning') 810 logging.info('main() returning')
OLDNEW
« no previous file with comments | « appengine/swarming/swarming_bot/bot_code/bot_auth.py ('k') | appengine/swarming/swarming_bot/bot_code/bot_main_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698