1# Copyright 2015 The Chromium OS 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 5import logging 6import time 7import sys 8 9from multiprocessing import Process 10from autotest_lib.client.bin import utils 11from autotest_lib.client.common_lib import error 12from autotest_lib.client.cros.faft.utils import shell_wrapper 13 14class ConnectionError(Exception): 15 """Raised on an error of connecting DUT.""" 16 pass 17 18 19class _BaseFwBypasser(object): 20 """Base class that controls bypass logic for firmware screens.""" 21 22 def __init__(self, faft_framework): 23 self.servo = faft_framework.servo 24 self.faft_config = faft_framework.faft_config 25 self.client_host = faft_framework._client 26 27 28 def bypass_dev_mode(self): 29 """Bypass the dev mode firmware logic to boot internal image.""" 30 raise NotImplementedError 31 32 33 def bypass_dev_boot_usb(self): 34 """Bypass the dev mode firmware logic to boot USB.""" 35 raise NotImplementedError 36 37 38 def bypass_rec_mode(self): 39 """Bypass the rec mode firmware logic to boot USB.""" 40 raise NotImplementedError 41 42 43 def trigger_dev_to_rec(self): 44 """Trigger to the rec mode from the dev screen.""" 45 raise NotImplementedError 46 47 48 def trigger_rec_to_dev(self): 49 """Trigger to the dev mode from the rec screen.""" 50 raise NotImplementedError 51 52 53 def trigger_dev_to_normal(self): 54 """Trigger to the normal mode from the dev screen.""" 55 raise NotImplementedError 56 57 58class _CtrlDBypasser(_BaseFwBypasser): 59 """Controls bypass logic via Ctrl-D combo.""" 60 61 def bypass_dev_mode(self): 62 """Bypass the dev mode firmware logic to boot internal image.""" 63 time.sleep(self.faft_config.firmware_screen) 64 self.servo.ctrl_d() 65 66 67 def bypass_dev_boot_usb(self): 68 """Bypass the dev mode firmware logic to boot USB.""" 69 time.sleep(self.faft_config.firmware_screen) 70 self.servo.ctrl_u() 71 72 73 def bypass_rec_mode(self): 74 """Bypass the rec mode firmware logic to boot USB.""" 75 self.servo.switch_usbkey('host') 76 time.sleep(self.faft_config.usb_plug) 77 self.servo.switch_usbkey('dut') 78 if not self.client_host.ping_wait_up( 79 timeout=self.faft_config.delay_reboot_to_ping): 80 psc = self.servo.get_power_state_controller() 81 psc.power_on(psc.REC_ON) 82 83 84 def trigger_dev_to_rec(self): 85 """Trigger to the rec mode from the dev screen.""" 86 time.sleep(self.faft_config.firmware_screen) 87 88 # Pressing Enter for too long triggers a second key press. 89 # Let's press it without delay 90 self.servo.enter_key(press_secs=0) 91 92 # For Alex/ZGB, there is a dev warning screen in text mode. 93 # Skip it by pressing Ctrl-D. 94 if self.faft_config.need_dev_transition: 95 time.sleep(self.faft_config.legacy_text_screen) 96 self.servo.ctrl_d() 97 98 99 def trigger_rec_to_dev(self): 100 """Trigger to the dev mode from the rec screen.""" 101 time.sleep(self.faft_config.firmware_screen) 102 self.servo.ctrl_d() 103 time.sleep(self.faft_config.confirm_screen) 104 if self.faft_config.rec_button_dev_switch: 105 logging.info('RECOVERY button pressed to switch to dev mode') 106 self.servo.toggle_recovery_switch() 107 else: 108 logging.info('ENTER pressed to switch to dev mode') 109 self.servo.enter_key() 110 111 112 def trigger_dev_to_normal(self): 113 """Trigger to the normal mode from the dev screen.""" 114 time.sleep(self.faft_config.firmware_screen) 115 self.servo.enter_key() 116 time.sleep(self.faft_config.confirm_screen) 117 self.servo.enter_key() 118 119 120class _JetstreamBypasser(_BaseFwBypasser): 121 """Controls bypass logic of Jetstream devices.""" 122 123 def bypass_dev_mode(self): 124 """Bypass the dev mode firmware logic to boot internal image.""" 125 # Jetstream does nothing to bypass. 126 pass 127 128 129 def bypass_dev_boot_usb(self): 130 """Bypass the dev mode firmware logic to boot USB.""" 131 # TODO: Confirm if it is a proper way to trigger dev boot USB. 132 # We can't verify it this time due to a bug that always boots into 133 # USB on dev mode. 134 self.servo.enable_development_mode() 135 self.servo.switch_usbkey('dut') 136 time.sleep(self.faft_config.firmware_screen) 137 self.servo.toggle_development_switch() 138 139 140 def bypass_rec_mode(self): 141 """Bypass the rec mode firmware logic to boot USB.""" 142 self.servo.switch_usbkey('host') 143 time.sleep(self.faft_config.usb_plug) 144 self.servo.switch_usbkey('dut') 145 146 147 def trigger_dev_to_rec(self): 148 """Trigger to the rec mode from the dev screen.""" 149 # Jetstream does not have this triggering logic. 150 raise NotImplementedError 151 152 153 def trigger_rec_to_dev(self): 154 """Trigger to the dev mode from the rec screen.""" 155 self.servo.disable_development_mode() 156 time.sleep(self.faft_config.firmware_screen) 157 self.servo.toggle_development_switch() 158 159 160 def trigger_dev_to_normal(self): 161 """Trigger to the normal mode from the dev screen.""" 162 # Jetstream does not have this triggering logic. 163 raise NotImplementedError 164 165 166def _create_fw_bypasser(faft_framework): 167 """Creates a proper firmware bypasser. 168 169 @param faft_framework: The main FAFT framework object. 170 """ 171 bypasser_type = faft_framework.faft_config.fw_bypasser_type 172 if bypasser_type == 'ctrl_d_bypasser': 173 logging.info('Create a CtrlDBypasser') 174 return _CtrlDBypasser(faft_framework) 175 elif bypasser_type == 'jetstream_bypasser': 176 logging.info('Create a JetstreamBypasser') 177 return _JetstreamBypasser(faft_framework) 178 elif bypasser_type == 'ryu_bypasser': 179 # FIXME Create an RyuBypasser 180 logging.info('Create a CtrlDBypasser') 181 return _CtrlDBypasser(faft_framework) 182 else: 183 raise NotImplementedError('Not supported fw_bypasser_type: %s', 184 bypasser_type) 185 186 187class _BaseModeSwitcher(object): 188 """Base class that controls firmware mode switching.""" 189 190 def __init__(self, faft_framework): 191 self.faft_framework = faft_framework 192 self.client_host = faft_framework._client 193 self.faft_client = faft_framework.faft_client 194 self.servo = faft_framework.servo 195 self.faft_config = faft_framework.faft_config 196 self.checkers = faft_framework.checkers 197 self.bypasser = _create_fw_bypasser(faft_framework) 198 self._backup_mode = None 199 200 201 def setup_mode(self, mode): 202 """Setup for the requested mode. 203 204 It makes sure the system in the requested mode. If not, it tries to 205 do so. 206 207 @param mode: A string of mode, one of 'normal', 'dev', or 'rec'. 208 """ 209 if not self.checkers.mode_checker(mode): 210 logging.info('System not in expected %s mode. Reboot into it.', 211 mode) 212 if self._backup_mode is None: 213 # Only resume to normal/dev mode after test, not recovery. 214 self._backup_mode = 'dev' if mode == 'normal' else 'normal' 215 self.reboot_to_mode(mode) 216 217 218 def restore_mode(self): 219 """Restores original dev mode status if it has changed.""" 220 if self._backup_mode is not None: 221 self.reboot_to_mode(self._backup_mode) 222 223 224 def reboot_to_mode(self, to_mode, from_mode=None, sync_before_boot=True, 225 wait_for_dut_up=True): 226 """Reboot and execute the mode switching sequence. 227 228 @param to_mode: The target mode, one of 'normal', 'dev', or 'rec'. 229 @param from_mode: The original mode, optional, one of 'normal, 'dev', 230 or 'rec'. 231 @param sync_before_boot: True to sync to disk before booting. 232 @param wait_for_dut_up: True to wait DUT online again. False to do the 233 reboot and mode switching sequence only and may 234 need more operations to pass the firmware 235 screen. 236 """ 237 logging.info('-[ModeSwitcher]-[ start reboot_to_mode(%r, %r, %r) ]-', 238 to_mode, from_mode, wait_for_dut_up) 239 if sync_before_boot: 240 self.faft_framework.blocking_sync() 241 if to_mode == 'rec': 242 self._enable_rec_mode_and_reboot(usb_state='dut') 243 if wait_for_dut_up: 244 self.bypass_rec_mode() 245 self.wait_for_client() 246 247 elif to_mode == 'dev': 248 self._enable_dev_mode_and_reboot() 249 if wait_for_dut_up: 250 self.bypass_dev_mode() 251 self.wait_for_client() 252 253 elif to_mode == 'normal': 254 self._enable_normal_mode_and_reboot() 255 if wait_for_dut_up: 256 self.wait_for_client() 257 258 else: 259 raise NotImplementedError( 260 'Not supported mode switching from %s to %s' % 261 (str(from_mode), to_mode)) 262 logging.info('-[ModeSwitcher]-[ end reboot_to_mode(%r, %r, %r) ]-', 263 to_mode, from_mode, wait_for_dut_up) 264 265 266 def mode_aware_reboot(self, reboot_type=None, reboot_method=None, 267 sync_before_boot=True, wait_for_dut_up=True): 268 """Uses a mode-aware way to reboot DUT. 269 270 For example, if DUT is in dev mode, it requires pressing Ctrl-D to 271 bypass the developer screen. 272 273 @param reboot_type: A string of reboot type, one of 'warm', 'cold', or 274 'custom'. Default is a warm reboot. 275 @param reboot_method: A custom method to do the reboot. Only use it if 276 reboot_type='custom'. 277 @param sync_before_boot: True to sync to disk before booting. 278 @param wait_for_dut_up: True to wait DUT online again. False to do the 279 reboot only. 280 """ 281 if reboot_type is None or reboot_type == 'warm': 282 reboot_method = self.servo.get_power_state_controller().warm_reset 283 elif reboot_type == 'cold': 284 reboot_method = self.servo.get_power_state_controller().reset 285 elif reboot_type != 'custom': 286 raise NotImplementedError('Not supported reboot_type: %s', 287 reboot_type) 288 289 logging.info("-[ModeSwitcher]-[ start mode_aware_reboot(%r, %s, ..) ]-", 290 reboot_type, reboot_method.__name__) 291 is_normal = is_dev = False 292 if sync_before_boot: 293 if wait_for_dut_up: 294 is_normal = self.checkers.mode_checker('normal') 295 is_dev = self.checkers.mode_checker('dev') 296 boot_id = self.faft_framework.get_bootid() 297 self.faft_framework.blocking_sync() 298 logging.info("-[mode_aware_reboot]-[ is_normal=%s is_dev=%s ]-", 299 is_normal, is_dev); 300 reboot_method() 301 if sync_before_boot: 302 self.wait_for_client_offline(orig_boot_id=boot_id) 303 if wait_for_dut_up: 304 # For encapsulating the behavior of skipping firmware screen, 305 # e.g. requiring unplug and plug USB, the variants are not 306 # hard coded in tests. We keep this logic in this 307 # mode_aware_reboot method. 308 if not is_dev: 309 self.servo.switch_usbkey('host') 310 if not is_normal: 311 self.bypass_dev_mode() 312 if not is_dev: 313 self.bypass_rec_mode() 314 self.wait_for_client() 315 logging.info("-[ModeSwitcher]-[ end mode_aware_reboot(%r, %s, ..) ]-", 316 reboot_type, reboot_method.__name__) 317 318 319 def _enable_rec_mode_and_reboot(self, usb_state=None): 320 """Switch to rec mode and reboot. 321 322 This method emulates the behavior of the old physical recovery switch, 323 i.e. switch ON + reboot + switch OFF, and the new keyboard controlled 324 recovery mode, i.e. just press Power + Esc + Refresh. 325 326 @param usb_state: A string, one of 'dut', 'host', or 'off'. 327 """ 328 psc = self.servo.get_power_state_controller() 329 psc.power_off() 330 if usb_state: 331 self.servo.switch_usbkey(usb_state) 332 psc.power_on(psc.REC_ON) 333 334 335 def _disable_rec_mode_and_reboot(self, usb_state=None): 336 """Disable the rec mode and reboot. 337 338 It is achieved by calling power state controller to do a normal 339 power on. 340 """ 341 psc = self.servo.get_power_state_controller() 342 psc.power_off() 343 psc.power_on(psc.REC_OFF) 344 345 346 def _enable_dev_mode_and_reboot(self): 347 """Switch to developer mode and reboot.""" 348 raise NotImplementedError 349 350 351 def _enable_normal_mode_and_reboot(self): 352 """Switch to normal mode and reboot.""" 353 raise NotImplementedError 354 355 356 # Redirects the following methods to FwBypasser 357 def bypass_dev_mode(self): 358 """Bypass the dev mode firmware logic to boot internal image.""" 359 logging.info("-[bypass_dev_mode]-") 360 self.bypasser.bypass_dev_mode() 361 362 363 def bypass_dev_boot_usb(self): 364 """Bypass the dev mode firmware logic to boot USB.""" 365 logging.info("-[bypass_dev_boot_usb]-") 366 self.bypasser.bypass_dev_boot_usb() 367 368 369 def bypass_rec_mode(self): 370 """Bypass the rec mode firmware logic to boot USB.""" 371 logging.info("-[bypass_rec_mode]-") 372 self.bypasser.bypass_rec_mode() 373 374 375 def trigger_dev_to_rec(self): 376 """Trigger to the rec mode from the dev screen.""" 377 self.bypasser.trigger_dev_to_rec() 378 379 380 def trigger_rec_to_dev(self): 381 """Trigger to the dev mode from the rec screen.""" 382 self.bypasser.trigger_rec_to_dev() 383 384 385 def trigger_dev_to_normal(self): 386 """Trigger to the normal mode from the dev screen.""" 387 self.bypasser.trigger_dev_to_normal() 388 389 390 def wait_for_client(self, timeout=180): 391 """Wait for the client to come back online. 392 393 New remote processes will be launched if their used flags are enabled. 394 395 @param timeout: Time in seconds to wait for the client SSH daemon to 396 come up. 397 @raise ConnectionError: Failed to connect DUT. 398 """ 399 logging.info("-[FAFT]-[ start wait_for_client ]---") 400 # Wait for the system to respond to ping before attempting ssh 401 if not self.client_host.ping_wait_up(timeout): 402 logging.warning("-[FAFT]-[ system did not respond to ping ]") 403 if self.client_host.wait_up(timeout): 404 # Check the FAFT client is avaiable. 405 self.faft_client.system.is_available() 406 # Stop update-engine as it may change firmware/kernel. 407 self.faft_framework._stop_service('update-engine') 408 else: 409 logging.error('wait_for_client() timed out.') 410 raise ConnectionError() 411 logging.info("-[FAFT]-[ end wait_for_client ]-----") 412 413 414 def wait_for_client_offline(self, timeout=60, orig_boot_id=None): 415 """Wait for the client to come offline. 416 417 @param timeout: Time in seconds to wait the client to come offline. 418 @param orig_boot_id: A string containing the original boot id. 419 @raise ConnectionError: Failed to wait DUT offline. 420 """ 421 # When running against panther, we see that sometimes 422 # ping_wait_down() does not work correctly. There needs to 423 # be some investigation to the root cause. 424 # If we sleep for 120s before running get_boot_id(), it 425 # does succeed. But if we change this to ping_wait_down() 426 # there are implications on the wait time when running 427 # commands at the fw screens. 428 if not self.client_host.ping_wait_down(timeout): 429 if orig_boot_id and self.client_host.get_boot_id() != orig_boot_id: 430 logging.warn('Reboot done very quickly.') 431 return 432 raise ConnectionError() 433 434 435class _PhysicalButtonSwitcher(_BaseModeSwitcher): 436 """Class that switches firmware mode via physical button.""" 437 438 def _enable_dev_mode_and_reboot(self): 439 """Switch to developer mode and reboot.""" 440 self.servo.enable_development_mode() 441 self.faft_client.system.run_shell_command( 442 'chromeos-firmwareupdate --mode todev && reboot') 443 444 445 def _enable_normal_mode_and_reboot(self): 446 """Switch to normal mode and reboot.""" 447 self.servo.disable_development_mode() 448 self.faft_client.system.run_shell_command( 449 'chromeos-firmwareupdate --mode tonormal && reboot') 450 451 452class _KeyboardDevSwitcher(_BaseModeSwitcher): 453 """Class that switches firmware mode via keyboard combo.""" 454 455 def _enable_dev_mode_and_reboot(self): 456 """Switch to developer mode and reboot.""" 457 logging.info("Enabling keyboard controlled developer mode") 458 # Rebooting EC with rec mode on. Should power on AP. 459 # Plug out USB disk for preventing recovery boot without warning 460 self._enable_rec_mode_and_reboot(usb_state='host') 461 self.wait_for_client_offline() 462 self.bypasser.trigger_rec_to_dev() 463 464 465 def _enable_normal_mode_and_reboot(self): 466 """Switch to normal mode and reboot.""" 467 logging.info("Disabling keyboard controlled developer mode") 468 self._disable_rec_mode_and_reboot() 469 self.wait_for_client_offline() 470 self.bypasser.trigger_dev_to_normal() 471 472 473class _JetstreamSwitcher(_BaseModeSwitcher): 474 """Class that switches firmware mode in Jetstream devices.""" 475 476 def _enable_dev_mode_and_reboot(self): 477 """Switch to developer mode and reboot.""" 478 logging.info("Enabling Jetstream developer mode") 479 self._enable_rec_mode_and_reboot(usb_state='host') 480 self.wait_for_client_offline() 481 self.bypasser.trigger_rec_to_dev() 482 483 484 def _enable_normal_mode_and_reboot(self): 485 """Switch to normal mode and reboot.""" 486 logging.info("Disabling Jetstream developer mode") 487 self.servo.disable_development_mode() 488 self._enable_rec_mode_and_reboot(usb_state='host') 489 time.sleep(self.faft_config.firmware_screen) 490 self._disable_rec_mode_and_reboot(usb_state='host') 491 492 493class _RyuSwitcher(_BaseModeSwitcher): 494 """Class that switches firmware mode via physical button.""" 495 496 FASTBOOT_OEM_DELAY = 10 497 RECOVERY_TIMEOUT = 2400 498 RECOVERY_SETUP = 60 499 ANDROID_BOOTUP = 600 500 FWTOOL_STARTUP_DELAY = 30 501 502 def wait_for_client(self, timeout=180): 503 """Wait for the client to come back online. 504 505 New remote processes will be launched if their used flags are enabled. 506 507 @param timeout: Time in seconds to wait for the client SSH daemon to 508 come up. 509 @raise ConnectionError: Failed to connect DUT. 510 """ 511 if not self.faft_client.system.wait_for_client(timeout): 512 raise ConnectionError() 513 514 # there's a conflict between fwtool and crossystem trying to access 515 # the nvram after the OS boots up. Temporarily put a hard wait of 516 # 30 seconds to try to wait for fwtool to finish up. 517 time.sleep(self.FWTOOL_STARTUP_DELAY) 518 519 520 def wait_for_client_offline(self, timeout=60, orig_boot_id=None): 521 """Wait for the client to come offline. 522 523 @param timeout: Time in seconds to wait the client to come offline. 524 @param orig_boot_id: A string containing the original boot id. 525 @raise ConnectionError: Failed to wait DUT offline. 526 """ 527 # TODO: Add a way to check orig_boot_id 528 if not self.faft_client.system.wait_for_client_offline(timeout): 529 raise ConnectionError() 530 531 def print_recovery_warning(self): 532 """Print recovery warning""" 533 logging.info("***") 534 logging.info("*** Entering recovery mode. This may take awhile ***") 535 logging.info("***") 536 # wait a minute for DUT to get settled into wipe stage 537 time.sleep(self.RECOVERY_SETUP) 538 539 def is_fastboot_mode(self): 540 """Return True if DUT in fastboot mode, False otherwise""" 541 result = self.faft_client.host.run_shell_command_get_output( 542 'fastboot devices') 543 if not result: 544 return False 545 else: 546 return True 547 548 def wait_for_client_fastboot(self, timeout=30): 549 """Wait for the client to come online in fastboot mode 550 551 @param timeout: Time in seconds to wait the client 552 @raise ConnectionError: Failed to wait DUT offline. 553 """ 554 utils.wait_for_value(self.is_fastboot_mode, True, timeout_sec=timeout) 555 556 def _run_cmd(self, args): 557 """Wrapper for run_shell_command 558 559 For Process creation 560 """ 561 return self.faft_client.host.run_shell_command(args) 562 563 def _enable_dev_mode_and_reboot(self): 564 """Switch to developer mode and reboot.""" 565 logging.info("Entering RyuSwitcher: _enable_dev_mode_and_reboot") 566 try: 567 self.faft_client.system.run_shell_command('reboot bootloader') 568 self.wait_for_client_fastboot() 569 570 process = Process( 571 target=self._run_cmd, 572 args=('fastboot oem unlock',)) 573 process.start() 574 575 # need a slight delay to give the ap time to get into valid state 576 time.sleep(self.FASTBOOT_OEM_DELAY) 577 self.servo.power_key(self.faft_config.hold_pwr_button_poweron) 578 process.join() 579 580 self.print_recovery_warning() 581 self.wait_for_client_fastboot(self.RECOVERY_TIMEOUT) 582 self.faft_client.host.run_shell_command('fastboot continue') 583 self.wait_for_client(self.ANDROID_BOOTUP) 584 585 # need to reset DUT into clean state 586 except shell_wrapper.ShellError: 587 raise error.TestError('Error executing shell command') 588 except ConnectionError: 589 raise error.TestError('Timed out waiting for DUT to exit recovery') 590 except: 591 raise error.TestError('Unexpected Exception: %s' % sys.exc_info()[0]) 592 logging.info("Exiting RyuSwitcher: _enable_dev_mode_and_reboot") 593 594 def _enable_normal_mode_and_reboot(self): 595 """Switch to normal mode and reboot.""" 596 try: 597 self.faft_client.system.run_shell_command('reboot bootloader') 598 self.wait_for_client_fastboot() 599 600 process = Process( 601 target=self._run_cmd, 602 args=('fastboot oem lock',)) 603 process.start() 604 605 # need a slight delay to give the ap time to get into valid state 606 time.sleep(self.FASTBOOT_OEM_DELAY) 607 self.servo.power_key(self.faft_config.hold_pwr_button_poweron) 608 process.join() 609 610 self.print_recovery_warning() 611 self.wait_for_client_fastboot(self.RECOVERY_TIMEOUT) 612 self.faft_client.host.run_shell_command('fastboot continue') 613 self.wait_for_client(self.ANDROID_BOOTUP) 614 615 # need to reset DUT into clean state 616 except shell_wrapper.ShellError: 617 raise error.TestError('Error executing shell command') 618 except ConnectionError: 619 raise error.TestError('Timed out waiting for DUT to exit recovery') 620 except: 621 raise error.TestError('Unexpected Exception: %s' % sys.exc_info()[0]) 622 logging.info("Exiting RyuSwitcher: _enable_normal_mode_and_reboot") 623 624def create_mode_switcher(faft_framework): 625 """Creates a proper mode switcher. 626 627 @param faft_framework: The main FAFT framework object. 628 """ 629 switcher_type = faft_framework.faft_config.mode_switcher_type 630 if switcher_type == 'physical_button_switcher': 631 logging.info('Create a PhysicalButtonSwitcher') 632 return _PhysicalButtonSwitcher(faft_framework) 633 elif switcher_type == 'keyboard_dev_switcher': 634 logging.info('Create a KeyboardDevSwitcher') 635 return _KeyboardDevSwitcher(faft_framework) 636 elif switcher_type == 'jetstream_switcher': 637 logging.info('Create a JetstreamSwitcher') 638 return _JetstreamSwitcher(faft_framework) 639 elif switcher_type == 'ryu_switcher': 640 logging.info('Create a RyuSwitcher') 641 return _RyuSwitcher(faft_framework) 642 else: 643 raise NotImplementedError('Not supported mode_switcher_type: %s', 644 switcher_type) 645