| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The LUCI Authors. All rights reserved. | 2 # Copyright 2015 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
| 5 | 5 |
| 6 """Starts a local bot to connect to a local server.""" | 6 """Starts a local bot to connect to a local server.""" |
| 7 | 7 |
| 8 import glob | 8 import glob |
| 9 import logging | 9 import logging |
| 10 import os | 10 import os |
| 11 import signal | 11 import signal |
| 12 import socket | 12 import socket |
| 13 import sys | 13 import sys |
| 14 import tempfile | 14 import tempfile |
| 15 import urllib | 15 import urllib |
| 16 | 16 |
| 17 | 17 |
| 18 THIS_DIR = os.path.dirname(os.path.abspath(__file__)) | 18 THIS_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 19 CLIENT_DIR = os.path.join(THIS_DIR, '..', '..', '..', 'client') | 19 CLIENT_DIR = os.path.join(THIS_DIR, '..', '..', '..', 'client') |
| 20 sys.path.insert(0, CLIENT_DIR) | 20 sys.path.insert(0, CLIENT_DIR) |
| 21 from third_party.depot_tools import fix_encoding | 21 from third_party.depot_tools import fix_encoding |
| 22 from utils import file_path | 22 from utils import file_path |
| 23 from utils import subprocess42 | 23 from utils import subprocess42 |
| 24 sys.path.pop(0) | 24 sys.path.pop(0) |
| 25 | 25 |
| 26 | 26 |
| 27 def _safe_rm(path): |
| 28 if os.path.exists(path): |
| 29 try: |
| 30 file_path.rmtree(path) |
| 31 except OSError as e: |
| 32 logging.error('Failed to delete %s: %s', path, e) |
| 33 |
| 34 |
| 27 class LocalBot(object): | 35 class LocalBot(object): |
| 28 """A local running Swarming bot. | 36 """A local running Swarming bot. |
| 29 | 37 |
| 30 It creates its own temporary directory to download the zip and run tasks | 38 It creates its own temporary directory to download the zip and run tasks |
| 31 locally. | 39 locally. |
| 32 """ | 40 """ |
| 33 def __init__(self, swarming_server_url, redirect=True): | 41 def __init__(self, swarming_server_url, redirect=True): |
| 34 self._tmpdir = tempfile.mkdtemp(prefix='swarming_bot') | 42 # It is deleted in self.stop(False). |
| 43 self._botdir = tempfile.mkdtemp(prefix='swarming_bot') |
| 35 self._swarming_server_url = swarming_server_url | 44 self._swarming_server_url = swarming_server_url |
| 36 self._proc = None | 45 self._proc = None |
| 37 self._logs = {} | 46 self._logs = {} |
| 38 self._redirect = redirect | 47 self._redirect = redirect |
| 39 | 48 |
| 40 def wipe_cache(self): | 49 def wipe_cache(self, restart): |
| 41 """Blows away this bot's cache.""" | 50 """Blows away this bot's cache and restart it. |
| 42 for i in ('c', 'isolated_cache'): | 51 |
| 43 cache_dir = os.path.join(self._tmpdir, i) | 52 There's just too much risk of the bot failing over so it's not worth not |
| 44 if os.path.exists(cache_dir): | 53 restarting it. |
| 45 try: | 54 """ |
| 46 file_path.rmtree(cache_dir) | 55 if restart: |
| 47 except OSError: | 56 logging.info('wipe_cache(): Restarting the bot') |
| 48 logging.info('Failed to deleted %s', cache_dir) | 57 self.stop(True) |
| 58 # Deletion needs to happen while the bot is not running to ensure no side |
| 59 # effect. |
| 60 # These values are from ./swarming_bot/bot_code/bot_main.py. |
| 61 _safe_rm(os.path.join(self._botdir, 'c')) |
| 62 _safe_rm(os.path.join(self._botdir, 'isolated_cache')) |
| 63 self.start() |
| 64 else: |
| 65 logging.info('wipe_cache(): wiping cache without telling the bot') |
| 66 _safe_rm(os.path.join(self._botdir, 'c')) |
| 67 _safe_rm(os.path.join(self._botdir, 'isolated_cache')) |
| 49 | 68 |
| 50 @property | 69 @property |
| 51 def bot_id(self): | 70 def bot_id(self): |
| 52 # TODO(maruel): Big assumption. | 71 # TODO(maruel): Big assumption. |
| 53 return socket.getfqdn().split('.')[0] | 72 return socket.getfqdn().split('.')[0] |
| 54 | 73 |
| 55 @property | 74 @property |
| 56 def log(self): | 75 def log(self): |
| 57 """Returns the log output. Only set after calling stop().""" | 76 """Returns the log output. Only set after calling stop().""" |
| 58 return '\n'.join(self._logs.itervalues()) if self._logs else None | 77 return '\n'.join(self._logs.itervalues()) if self._logs else None |
| 59 | 78 |
| 60 def start(self): | 79 def start(self): |
| 61 """Starts the local Swarming bot.""" | 80 """Starts the local Swarming bot.""" |
| 62 assert not self._proc | 81 assert not self._proc |
| 63 bot_zip = os.path.join(self._tmpdir, 'swarming_bot.zip') | 82 bot_zip = os.path.join(self._botdir, 'swarming_bot.zip') |
| 64 urllib.urlretrieve(self._swarming_server_url + '/bot_code', bot_zip) | 83 urllib.urlretrieve(self._swarming_server_url + '/bot_code', bot_zip) |
| 65 cmd = [sys.executable, bot_zip, 'start_slave'] | 84 cmd = [sys.executable, bot_zip, 'start_slave'] |
| 66 if self._redirect: | 85 if self._redirect: |
| 67 logs = os.path.join(self._tmpdir, 'logs') | 86 logs = os.path.join(self._botdir, 'logs') |
| 68 if not os.path.isdir(logs): | 87 if not os.path.isdir(logs): |
| 69 os.mkdir(logs) | 88 os.mkdir(logs) |
| 70 with open(os.path.join(logs, 'bot_stdout.log'), 'wb') as f: | 89 with open(os.path.join(logs, 'bot_stdout.log'), 'wb') as f: |
| 71 self._proc = subprocess42.Popen( | 90 self._proc = subprocess42.Popen( |
| 72 cmd, cwd=self._tmpdir, stdout=f, stderr=f, detached=True) | 91 cmd, cwd=self._botdir, stdout=f, stderr=f, detached=True) |
| 73 else: | 92 else: |
| 74 self._proc = subprocess42.Popen(cmd, cwd=self._tmpdir, detached=True) | 93 self._proc = subprocess42.Popen(cmd, cwd=self._botdir, detached=True) |
| 75 | 94 |
| 76 def stop(self, leak): | 95 def stop(self, leak): |
| 77 """Stops the local Swarming bot. Returns the process exit code.""" | 96 """Stops the local Swarming bot. Returns the process exit code.""" |
| 78 if not self._proc: | 97 if not self._proc: |
| 79 return None | 98 return None |
| 80 if self._proc.poll() is None: | 99 if self._proc.poll() is None: |
| 81 try: | 100 try: |
| 82 self._proc.send_signal(signal.SIGTERM) | 101 self._proc.send_signal(signal.SIGTERM) |
| 83 # TODO(maruel): SIGKILL after N seconds. | 102 # TODO(maruel): SIGKILL after N seconds. |
| 84 self._proc.wait() | 103 self._proc.wait() |
| 85 except OSError: | 104 except OSError: |
| 86 pass | 105 pass |
| 87 exit_code = self._proc.returncode | 106 exit_code = self._proc.returncode |
| 88 if self._tmpdir: | 107 if self._botdir: |
| 89 for i in sorted(glob.glob(os.path.join(self._tmpdir, 'logs', '*.log'))): | 108 for i in sorted(glob.glob(os.path.join(self._botdir, 'logs', '*.log'))): |
| 90 self._read_log(i) | 109 self._read_log(i) |
| 91 if not leak: | 110 if not leak: |
| 92 try: | 111 try: |
| 93 file_path.rmtree(self._tmpdir) | 112 file_path.rmtree(self._botdir) |
| 94 except OSError: | 113 except OSError: |
| 95 print >> sys.stderr, 'Leaking %s' % self._tmpdir | 114 print >> sys.stderr, 'Leaking %s' % self._botdir |
| 96 self._tmpdir = None | |
| 97 self._proc = None | 115 self._proc = None |
| 98 return exit_code | 116 return exit_code |
| 99 | 117 |
| 100 def poll(self): | 118 def poll(self): |
| 101 """Polls the process to know if it exited.""" | 119 """Polls the process to know if it exited.""" |
| 102 self._proc.poll() | 120 if self._proc: |
| 121 self._proc.poll() |
| 103 | 122 |
| 104 def wait(self, timeout=None): | 123 def wait(self, timeout=None): |
| 105 """Waits for the process to normally exit.""" | 124 """Waits for the process to normally exit.""" |
| 106 return self._proc.wait(timeout) | 125 if self._proc: |
| 126 return self._proc.wait(timeout) |
| 107 | 127 |
| 108 def kill(self): | 128 def kill(self): |
| 109 """Kills the child forcibly.""" | 129 """Kills the child forcibly.""" |
| 110 if self._proc: | 130 if self._proc: |
| 111 self._proc.kill() | 131 self._proc.kill() |
| 112 | 132 |
| 113 def dump_log(self): | 133 def dump_log(self): |
| 114 """Prints dev_appserver log to stderr, works only if app is stopped.""" | 134 """Prints dev_appserver log to stderr, works only if app is stopped.""" |
| 115 print >> sys.stderr, '-' * 60 | 135 print >> sys.stderr, '-' * 60 |
| 116 print >> sys.stderr, 'swarming_bot log' | 136 print >> sys.stderr, 'swarming_bot log' |
| (...skipping 27 matching lines...) Expand all Loading... |
| 144 bot.dump_log() | 164 bot.dump_log() |
| 145 except KeyboardInterrupt: | 165 except KeyboardInterrupt: |
| 146 print >> sys.stderr, '<Ctrl-C> received; stopping bot' | 166 print >> sys.stderr, '<Ctrl-C> received; stopping bot' |
| 147 finally: | 167 finally: |
| 148 exit_code = bot.stop(False) | 168 exit_code = bot.stop(False) |
| 149 return exit_code | 169 return exit_code |
| 150 | 170 |
| 151 | 171 |
| 152 if __name__ == '__main__': | 172 if __name__ == '__main__': |
| 153 sys.exit(main()) | 173 sys.exit(main()) |
| OLD | NEW |