Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame^] | 1 | # Copyright (c) 2012 The Chromium 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 | """Provides an interface to start and stop Android emulator. |
| 6 | |
| 7 | Emulator: The class provides the methods to launch/shutdown the emulator with |
| 8 | the android virtual device named 'avd_armeabi' . |
| 9 | """ |
| 10 | |
| 11 | import logging |
| 12 | import os |
| 13 | import signal |
| 14 | import subprocess |
| 15 | import time |
| 16 | |
| 17 | from devil.android import device_errors |
| 18 | from devil.android import device_utils |
| 19 | from devil.android.sdk import adb_wrapper |
| 20 | from devil.utils import cmd_helper |
| 21 | from pylib import constants |
| 22 | from pylib import pexpect |
| 23 | from pylib.utils import time_profile |
| 24 | |
| 25 | # Default sdcard size in the format of [amount][unit] |
| 26 | DEFAULT_SDCARD_SIZE = '512M' |
| 27 | # Default internal storage (MB) of emulator image |
| 28 | DEFAULT_STORAGE_SIZE = '1024M' |
| 29 | |
| 30 | # Each emulator has 60 secs of wait time for launching |
| 31 | _BOOT_WAIT_INTERVALS = 6 |
| 32 | _BOOT_WAIT_INTERVAL_TIME = 10 |
| 33 | |
| 34 | # Path for avd files and avd dir |
| 35 | _BASE_AVD_DIR = os.path.expanduser(os.path.join('~', '.android', 'avd')) |
| 36 | _TOOLS_ANDROID_PATH = os.path.join(constants.ANDROID_SDK_ROOT, |
| 37 | 'tools', 'android') |
| 38 | |
| 39 | # Template used to generate config.ini files for the emulator |
| 40 | CONFIG_TEMPLATE = """avd.ini.encoding=ISO-8859-1 |
| 41 | hw.dPad=no |
| 42 | hw.lcd.density=320 |
| 43 | sdcard.size={sdcard.size} |
| 44 | hw.cpu.arch={hw.cpu.arch} |
| 45 | hw.device.hash=-708107041 |
| 46 | hw.camera.back=none |
| 47 | disk.dataPartition.size=800M |
| 48 | hw.gpu.enabled={gpu} |
| 49 | skin.path=720x1280 |
| 50 | skin.dynamic=yes |
| 51 | hw.keyboard=yes |
| 52 | hw.ramSize=1024 |
| 53 | hw.device.manufacturer=Google |
| 54 | hw.sdCard=yes |
| 55 | hw.mainKeys=no |
| 56 | hw.accelerometer=yes |
| 57 | skin.name=720x1280 |
| 58 | abi.type={abi.type} |
| 59 | hw.trackBall=no |
| 60 | hw.device.name=Galaxy Nexus |
| 61 | hw.battery=yes |
| 62 | hw.sensors.proximity=yes |
| 63 | image.sysdir.1=system-images/android-{api.level}/default/{abi.type}/ |
| 64 | hw.sensors.orientation=yes |
| 65 | hw.audioInput=yes |
| 66 | hw.camera.front=none |
| 67 | hw.gps=yes |
| 68 | vm.heapSize=128 |
| 69 | {extras}""" |
| 70 | |
| 71 | CONFIG_REPLACEMENTS = { |
| 72 | 'x86': { |
| 73 | '{hw.cpu.arch}': 'x86', |
| 74 | '{abi.type}': 'x86', |
| 75 | '{extras}': '' |
| 76 | }, |
| 77 | 'arm': { |
| 78 | '{hw.cpu.arch}': 'arm', |
| 79 | '{abi.type}': 'armeabi-v7a', |
| 80 | '{extras}': 'hw.cpu.model=cortex-a8\n' |
| 81 | }, |
| 82 | 'mips': { |
| 83 | '{hw.cpu.arch}': 'mips', |
| 84 | '{abi.type}': 'mips', |
| 85 | '{extras}': '' |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | class EmulatorLaunchException(Exception): |
| 90 | """Emulator failed to launch.""" |
| 91 | pass |
| 92 | |
| 93 | def WaitForEmulatorLaunch(num): |
| 94 | """Wait for emulators to finish booting |
| 95 | |
| 96 | Emulators on bots are launch with a separate background process, to avoid |
| 97 | running tests before the emulators are fully booted, this function waits for |
| 98 | a number of emulators to finish booting |
| 99 | |
| 100 | Arg: |
| 101 | num: the amount of emulators to wait. |
| 102 | """ |
| 103 | for _ in range(num*_BOOT_WAIT_INTERVALS): |
| 104 | emulators = [device_utils.DeviceUtils(a) |
| 105 | for a in adb_wrapper.AdbWrapper.Devices() |
| 106 | if a.is_emulator] |
| 107 | if len(emulators) >= num: |
| 108 | logging.info('All %d emulators launched', num) |
| 109 | return |
| 110 | logging.info( |
| 111 | 'Waiting for %d emulators, %d of them already launched', num, |
| 112 | len(emulators)) |
| 113 | time.sleep(_BOOT_WAIT_INTERVAL_TIME) |
| 114 | raise Exception("Expected %d emulators, %d launched within time limit" % |
| 115 | (num, len(emulators))) |
| 116 | |
| 117 | def KillAllEmulators(): |
| 118 | """Kill all running emulators that look like ones we started. |
| 119 | |
| 120 | There are odd 'sticky' cases where there can be no emulator process |
| 121 | running but a device slot is taken. A little bot trouble and we're out of |
| 122 | room forever. |
| 123 | """ |
| 124 | logging.info('Killing all existing emulators and existing the program') |
| 125 | emulators = [device_utils.DeviceUtils(a) |
| 126 | for a in adb_wrapper.AdbWrapper.Devices() |
| 127 | if a.is_emulator] |
| 128 | if not emulators: |
| 129 | return |
| 130 | for e in emulators: |
| 131 | e.adb.Emu(['kill']) |
| 132 | logging.info('Emulator killing is async; give a few seconds for all to die.') |
| 133 | for _ in range(10): |
| 134 | if not any(a.is_emulator for a in adb_wrapper.AdbWrapper.Devices()): |
| 135 | return |
| 136 | time.sleep(1) |
| 137 | |
| 138 | |
| 139 | def DeleteAllTempAVDs(): |
| 140 | """Delete all temporary AVDs which are created for tests. |
| 141 | |
| 142 | If the test exits abnormally and some temporary AVDs created when testing may |
| 143 | be left in the system. Clean these AVDs. |
| 144 | """ |
| 145 | logging.info('Deleting all the avd files') |
| 146 | avds = device_utils.GetAVDs() |
| 147 | if not avds: |
| 148 | return |
| 149 | for avd_name in avds: |
| 150 | if 'run_tests_avd' in avd_name: |
| 151 | cmd = [_TOOLS_ANDROID_PATH, '-s', 'delete', 'avd', '--name', avd_name] |
| 152 | cmd_helper.RunCmd(cmd) |
| 153 | logging.info('Delete AVD %s', avd_name) |
| 154 | |
| 155 | |
| 156 | class PortPool(object): |
| 157 | """Pool for emulator port starting position that changes over time.""" |
| 158 | _port_min = 5554 |
| 159 | _port_max = 5585 |
| 160 | _port_current_index = 0 |
| 161 | |
| 162 | @classmethod |
| 163 | def port_range(cls): |
| 164 | """Return a range of valid ports for emulator use. |
| 165 | |
| 166 | The port must be an even number between 5554 and 5584. Sometimes |
| 167 | a killed emulator "hangs on" to a port long enough to prevent |
| 168 | relaunch. This is especially true on slow machines (like a bot). |
| 169 | Cycling through a port start position helps make us resilient.""" |
| 170 | ports = range(cls._port_min, cls._port_max, 2) |
| 171 | n = cls._port_current_index |
| 172 | cls._port_current_index = (n + 1) % len(ports) |
| 173 | return ports[n:] + ports[:n] |
| 174 | |
| 175 | |
| 176 | def _GetAvailablePort(): |
| 177 | """Returns an available TCP port for the console.""" |
| 178 | used_ports = [] |
| 179 | emulators = [device_utils.DeviceUtils(a) |
| 180 | for a in adb_wrapper.AdbWrapper.Devices() |
| 181 | if a.is_emulator] |
| 182 | for emulator in emulators: |
| 183 | used_ports.append(emulator.adb.GetDeviceSerial().split('-')[1]) |
| 184 | for port in PortPool.port_range(): |
| 185 | if str(port) not in used_ports: |
| 186 | return port |
| 187 | |
| 188 | |
| 189 | def LaunchTempEmulators(emulator_count, abi, api_level, enable_kvm=False, |
| 190 | kill_and_launch=True, sdcard_size=DEFAULT_SDCARD_SIZE, |
| 191 | storage_size=DEFAULT_STORAGE_SIZE, wait_for_boot=True, |
| 192 | headless=False): |
| 193 | """Create and launch temporary emulators and wait for them to boot. |
| 194 | |
| 195 | Args: |
| 196 | emulator_count: number of emulators to launch. |
| 197 | abi: the emulator target platform |
| 198 | api_level: the api level (e.g., 19 for Android v4.4 - KitKat release) |
| 199 | wait_for_boot: whether or not to wait for emulators to boot up |
| 200 | headless: running emulator with no ui |
| 201 | |
| 202 | Returns: |
| 203 | List of emulators. |
| 204 | """ |
| 205 | emulators = [] |
| 206 | for n in xrange(emulator_count): |
| 207 | t = time_profile.TimeProfile('Emulator launch %d' % n) |
| 208 | # Creates a temporary AVD. |
| 209 | avd_name = 'run_tests_avd_%d' % n |
| 210 | logging.info('Emulator launch %d with avd_name=%s and api=%d', |
| 211 | n, avd_name, api_level) |
| 212 | emulator = Emulator(avd_name, abi, enable_kvm=enable_kvm, |
| 213 | sdcard_size=sdcard_size, storage_size=storage_size, |
| 214 | headless=headless) |
| 215 | emulator.CreateAVD(api_level) |
| 216 | emulator.Launch(kill_all_emulators=(n == 0 and kill_and_launch)) |
| 217 | t.Stop() |
| 218 | emulators.append(emulator) |
| 219 | # Wait for all emulators to boot completed. |
| 220 | if wait_for_boot: |
| 221 | for emulator in emulators: |
| 222 | emulator.ConfirmLaunch(True) |
| 223 | logging.info('All emulators are fully booted') |
| 224 | return emulators |
| 225 | |
| 226 | |
| 227 | def LaunchEmulator(avd_name, abi, kill_and_launch=True, enable_kvm=False, |
| 228 | sdcard_size=DEFAULT_SDCARD_SIZE, |
| 229 | storage_size=DEFAULT_STORAGE_SIZE, headless=False): |
| 230 | """Launch an existing emulator with name avd_name. |
| 231 | |
| 232 | Args: |
| 233 | avd_name: name of existing emulator |
| 234 | abi: the emulator target platform |
| 235 | headless: running emulator with no ui |
| 236 | |
| 237 | Returns: |
| 238 | emulator object. |
| 239 | """ |
| 240 | logging.info('Specified emulator named avd_name=%s launched', avd_name) |
| 241 | emulator = Emulator(avd_name, abi, enable_kvm=enable_kvm, |
| 242 | sdcard_size=sdcard_size, storage_size=storage_size, |
| 243 | headless=headless) |
| 244 | emulator.Launch(kill_all_emulators=kill_and_launch) |
| 245 | emulator.ConfirmLaunch(True) |
| 246 | return emulator |
| 247 | |
| 248 | |
| 249 | class Emulator(object): |
| 250 | """Provides the methods to launch/shutdown the emulator. |
| 251 | |
| 252 | The emulator has the android virtual device named 'avd_armeabi'. |
| 253 | |
| 254 | The emulator could use any even TCP port between 5554 and 5584 for the |
| 255 | console communication, and this port will be part of the device name like |
| 256 | 'emulator-5554'. Assume it is always True, as the device name is the id of |
| 257 | emulator managed in this class. |
| 258 | |
| 259 | Attributes: |
| 260 | emulator: Path of Android's emulator tool. |
| 261 | popen: Popen object of the running emulator process. |
| 262 | device: Device name of this emulator. |
| 263 | """ |
| 264 | |
| 265 | # Signals we listen for to kill the emulator on |
| 266 | _SIGNALS = (signal.SIGINT, signal.SIGHUP) |
| 267 | |
| 268 | # Time to wait for an emulator launch, in seconds. This includes |
| 269 | # the time to launch the emulator and a wait-for-device command. |
| 270 | _LAUNCH_TIMEOUT = 120 |
| 271 | |
| 272 | # Timeout interval of wait-for-device command before bouncing to a a |
| 273 | # process life check. |
| 274 | _WAITFORDEVICE_TIMEOUT = 5 |
| 275 | |
| 276 | # Time to wait for a 'wait for boot complete' (property set on device). |
| 277 | _WAITFORBOOT_TIMEOUT = 300 |
| 278 | |
| 279 | def __init__(self, avd_name, abi, enable_kvm=False, |
| 280 | sdcard_size=DEFAULT_SDCARD_SIZE, |
| 281 | storage_size=DEFAULT_STORAGE_SIZE, headless=False): |
| 282 | """Init an Emulator. |
| 283 | |
| 284 | Args: |
| 285 | avd_name: name of the AVD to create |
| 286 | abi: target platform for emulator being created, defaults to x86 |
| 287 | """ |
| 288 | android_sdk_root = constants.ANDROID_SDK_ROOT |
| 289 | self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator') |
| 290 | self.android = _TOOLS_ANDROID_PATH |
| 291 | self.popen = None |
| 292 | self.device_serial = None |
| 293 | self.abi = abi |
| 294 | self.avd_name = avd_name |
| 295 | self.sdcard_size = sdcard_size |
| 296 | self.storage_size = storage_size |
| 297 | self.enable_kvm = enable_kvm |
| 298 | self.headless = headless |
| 299 | |
| 300 | @staticmethod |
| 301 | def _DeviceName(): |
| 302 | """Return our device name.""" |
| 303 | port = _GetAvailablePort() |
| 304 | return ('emulator-%d' % port, port) |
| 305 | |
| 306 | def CreateAVD(self, api_level): |
| 307 | """Creates an AVD with the given name. |
| 308 | |
| 309 | Args: |
| 310 | api_level: the api level of the image |
| 311 | |
| 312 | Return avd_name. |
| 313 | """ |
| 314 | |
| 315 | if self.abi == 'arm': |
| 316 | abi_option = 'armeabi-v7a' |
| 317 | elif self.abi == 'mips': |
| 318 | abi_option = 'mips' |
| 319 | else: |
| 320 | abi_option = 'x86' |
| 321 | |
| 322 | api_target = 'android-%s' % api_level |
| 323 | |
| 324 | avd_command = [ |
| 325 | self.android, |
| 326 | '--silent', |
| 327 | 'create', 'avd', |
| 328 | '--name', self.avd_name, |
| 329 | '--abi', abi_option, |
| 330 | '--target', api_target, |
| 331 | '--sdcard', self.sdcard_size, |
| 332 | '--force', |
| 333 | ] |
| 334 | avd_cmd_str = ' '.join(avd_command) |
| 335 | logging.info('Create AVD command: %s', avd_cmd_str) |
| 336 | avd_process = pexpect.spawn(avd_cmd_str) |
| 337 | |
| 338 | # Instead of creating a custom profile, we overwrite config files. |
| 339 | avd_process.expect('Do you wish to create a custom hardware profile') |
| 340 | avd_process.sendline('no\n') |
| 341 | avd_process.expect('Created AVD \'%s\'' % self.avd_name) |
| 342 | |
| 343 | # Replace current configuration with default Galaxy Nexus config. |
| 344 | ini_file = os.path.join(_BASE_AVD_DIR, '%s.ini' % self.avd_name) |
| 345 | new_config_ini = os.path.join(_BASE_AVD_DIR, '%s.avd' % self.avd_name, |
| 346 | 'config.ini') |
| 347 | |
| 348 | # Remove config files with defaults to replace with Google's GN settings. |
| 349 | os.unlink(ini_file) |
| 350 | os.unlink(new_config_ini) |
| 351 | |
| 352 | # Create new configuration files with Galaxy Nexus by Google settings. |
| 353 | with open(ini_file, 'w') as new_ini: |
| 354 | new_ini.write('avd.ini.encoding=ISO-8859-1\n') |
| 355 | new_ini.write('target=%s\n' % api_target) |
| 356 | new_ini.write('path=%s/%s.avd\n' % (_BASE_AVD_DIR, self.avd_name)) |
| 357 | new_ini.write('path.rel=avd/%s.avd\n' % self.avd_name) |
| 358 | |
| 359 | custom_config = CONFIG_TEMPLATE |
| 360 | replacements = CONFIG_REPLACEMENTS[self.abi] |
| 361 | for key in replacements: |
| 362 | custom_config = custom_config.replace(key, replacements[key]) |
| 363 | custom_config = custom_config.replace('{api.level}', str(api_level)) |
| 364 | custom_config = custom_config.replace('{sdcard.size}', self.sdcard_size) |
| 365 | custom_config.replace('{gpu}', 'no' if self.headless else 'yes') |
| 366 | |
| 367 | with open(new_config_ini, 'w') as new_config_ini: |
| 368 | new_config_ini.write(custom_config) |
| 369 | |
| 370 | return self.avd_name |
| 371 | |
| 372 | |
| 373 | def _DeleteAVD(self): |
| 374 | """Delete the AVD of this emulator.""" |
| 375 | avd_command = [ |
| 376 | self.android, |
| 377 | '--silent', |
| 378 | 'delete', |
| 379 | 'avd', |
| 380 | '--name', self.avd_name, |
| 381 | ] |
| 382 | logging.info('Delete AVD command: %s', ' '.join(avd_command)) |
| 383 | cmd_helper.RunCmd(avd_command) |
| 384 | |
| 385 | def ResizeAndWipeAvd(self, storage_size): |
| 386 | """Wipes old AVD and creates new AVD of size |storage_size|. |
| 387 | |
| 388 | This serves as a work around for '-partition-size' and '-wipe-data' |
| 389 | """ |
| 390 | userdata_img = os.path.join(_BASE_AVD_DIR, '%s.avd' % self.avd_name, |
| 391 | 'userdata.img') |
| 392 | userdata_qemu_img = os.path.join(_BASE_AVD_DIR, '%s.avd' % self.avd_name, |
| 393 | 'userdata-qemu.img') |
| 394 | resize_cmd = ['resize2fs', userdata_img, '%s' % storage_size] |
| 395 | logging.info('Resizing userdata.img to ideal size') |
| 396 | cmd_helper.RunCmd(resize_cmd) |
| 397 | wipe_cmd = ['cp', userdata_img, userdata_qemu_img] |
| 398 | logging.info('Replacing userdata-qemu.img with the new userdata.img') |
| 399 | cmd_helper.RunCmd(wipe_cmd) |
| 400 | |
| 401 | def Launch(self, kill_all_emulators): |
| 402 | """Launches the emulator asynchronously. Call ConfirmLaunch() to ensure the |
| 403 | emulator is ready for use. |
| 404 | |
| 405 | If fails, an exception will be raised. |
| 406 | """ |
| 407 | if kill_all_emulators: |
| 408 | KillAllEmulators() # just to be sure |
| 409 | self._AggressiveImageCleanup() |
| 410 | (self.device_serial, port) = self._DeviceName() |
| 411 | self.ResizeAndWipeAvd(storage_size=self.storage_size) |
| 412 | emulator_command = [ |
| 413 | self.emulator, |
| 414 | # Speed up emulator launch by 40%. Really. |
| 415 | '-no-boot-anim', |
| 416 | ] |
| 417 | if self.headless: |
| 418 | emulator_command.extend([ |
| 419 | '-no-skin', |
| 420 | '-no-audio', |
| 421 | '-no-window' |
| 422 | ]) |
| 423 | else: |
| 424 | emulator_command.extend([ |
| 425 | '-gpu', 'on' |
| 426 | ]) |
| 427 | emulator_command.extend([ |
| 428 | # Use a familiar name and port. |
| 429 | '-avd', self.avd_name, |
| 430 | '-port', str(port), |
| 431 | # all the argument after qemu are sub arguments for qemu |
| 432 | '-qemu', '-m', '1024', |
| 433 | ]) |
| 434 | if self.abi == 'x86' and self.enable_kvm: |
| 435 | emulator_command.extend([ |
| 436 | # For x86 emulator --enable-kvm will fail early, avoiding accidental |
| 437 | # runs in a slow mode (i.e. without hardware virtualization support). |
| 438 | '--enable-kvm', |
| 439 | ]) |
| 440 | |
| 441 | logging.info('Emulator launch command: %s', ' '.join(emulator_command)) |
| 442 | self.popen = subprocess.Popen(args=emulator_command, |
| 443 | stderr=subprocess.STDOUT) |
| 444 | self._InstallKillHandler() |
| 445 | |
| 446 | @staticmethod |
| 447 | def _AggressiveImageCleanup(): |
| 448 | """Aggressive cleanup of emulator images. |
| 449 | |
| 450 | Experimentally it looks like our current emulator use on the bot |
| 451 | leaves image files around in /tmp/android-$USER. If a "random" |
| 452 | name gets reused, we choke with a 'File exists' error. |
| 453 | TODO(jrg): is there a less hacky way to accomplish the same goal? |
| 454 | """ |
| 455 | logging.info('Aggressive Image Cleanup') |
| 456 | emulator_imagedir = '/tmp/android-%s' % os.environ['USER'] |
| 457 | if not os.path.exists(emulator_imagedir): |
| 458 | return |
| 459 | for image in os.listdir(emulator_imagedir): |
| 460 | full_name = os.path.join(emulator_imagedir, image) |
| 461 | if 'emulator' in full_name: |
| 462 | logging.info('Deleting emulator image %s', full_name) |
| 463 | os.unlink(full_name) |
| 464 | |
| 465 | def ConfirmLaunch(self, wait_for_boot=False): |
| 466 | """Confirm the emulator launched properly. |
| 467 | |
| 468 | Loop on a wait-for-device with a very small timeout. On each |
| 469 | timeout, check the emulator process is still alive. |
| 470 | After confirming a wait-for-device can be successful, make sure |
| 471 | it returns the right answer. |
| 472 | """ |
| 473 | seconds_waited = 0 |
| 474 | number_of_waits = 2 # Make sure we can wfd twice |
| 475 | |
| 476 | device = device_utils.DeviceUtils(self.device_serial) |
| 477 | while seconds_waited < self._LAUNCH_TIMEOUT: |
| 478 | try: |
| 479 | device.adb.WaitForDevice( |
| 480 | timeout=self._WAITFORDEVICE_TIMEOUT, retries=1) |
| 481 | number_of_waits -= 1 |
| 482 | if not number_of_waits: |
| 483 | break |
| 484 | except device_errors.CommandTimeoutError: |
| 485 | seconds_waited += self._WAITFORDEVICE_TIMEOUT |
| 486 | device.adb.KillServer() |
| 487 | self.popen.poll() |
| 488 | if self.popen.returncode != None: |
| 489 | raise EmulatorLaunchException('EMULATOR DIED') |
| 490 | |
| 491 | if seconds_waited >= self._LAUNCH_TIMEOUT: |
| 492 | raise EmulatorLaunchException('TIMEOUT with wait-for-device') |
| 493 | |
| 494 | logging.info('Seconds waited on wait-for-device: %d', seconds_waited) |
| 495 | if wait_for_boot: |
| 496 | # Now that we checked for obvious problems, wait for a boot complete. |
| 497 | # Waiting for the package manager is sometimes problematic. |
| 498 | device.WaitUntilFullyBooted(timeout=self._WAITFORBOOT_TIMEOUT) |
| 499 | logging.info('%s is now fully booted', self.avd_name) |
| 500 | |
| 501 | def Shutdown(self): |
| 502 | """Shuts down the process started by launch.""" |
| 503 | self._DeleteAVD() |
| 504 | if self.popen: |
| 505 | self.popen.poll() |
| 506 | if self.popen.returncode == None: |
| 507 | self.popen.kill() |
| 508 | self.popen = None |
| 509 | |
| 510 | def _ShutdownOnSignal(self, _signum, _frame): |
| 511 | logging.critical('emulator _ShutdownOnSignal') |
| 512 | for sig in self._SIGNALS: |
| 513 | signal.signal(sig, signal.SIG_DFL) |
| 514 | self.Shutdown() |
| 515 | raise KeyboardInterrupt # print a stack |
| 516 | |
| 517 | def _InstallKillHandler(self): |
| 518 | """Install a handler to kill the emulator when we exit unexpectedly.""" |
| 519 | for sig in self._SIGNALS: |
| 520 | signal.signal(sig, self._ShutdownOnSignal) |