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