Home | History | Annotate | Download | only in cros
      1 # Copyright (c) 2012 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 
      5 """Provides utility methods for controlling powerd in ChromiumOS."""
      6 
      7 import errno
      8 import logging
      9 import multiprocessing
     10 import os
     11 import rtc
     12 import time
     13 import upstart
     14 
     15 SYSFS_POWER_STATE = '/sys/power/state'
     16 SYSFS_WAKEUP_COUNT = '/sys/power/wakeup_count'
     17 
     18 
     19 class SuspendFailure(Exception):
     20     """Base class for a failure during a single suspend/resume cycle."""
     21     pass
     22 
     23 
     24 class SuspendTimeout(SuspendFailure):
     25     """Suspend took too long, got wakeup event (RTC tick) before it was done."""
     26     pass
     27 
     28 
     29 class KernelError(SuspendFailure):
     30     """Kernel problem encountered during suspend/resume."""
     31     WHITELIST = [
     32             # crosbug.com/37594: debug tracing clock desync we don't care about
     33             (r'kernel/trace/ring_buffer.c:\d+ rb_reserve_next_event',
     34              r'Delta way too big!'),
     35         ]
     36 
     37 
     38 class FirmwareError(SuspendFailure):
     39     """String 'ERROR' found in firmware log after resume."""
     40     WHITELIST = [
     41             # crosbug.com/36762: no one knows, but it has always been there
     42             ('^stumpy', r'PNP: 002e\.4 70 irq size: 0x0000000001 not assigned'),
     43             # crbug.com/221538: no one knows what ME errors mean anyway
     44             ('^parrot', r'ME failed to respond'),
     45         ]
     46 
     47 
     48 class SpuriousWakeupError(SuspendFailure):
     49     """Received spurious wakeup while suspending or woke before schedule."""
     50     S3_WHITELIST = [  # (<board>, <eventlog wake source>, <syslog wake source>)
     51             # crbug.com/220014: spurious trackpad IRQs
     52             ('^link', 'Wake Source | GPIO | 12', ''),
     53             # crbug.com/345327: unknown, probably same as crbug.com/290923
     54             ('^x86-alex', '', ''),   # alex can report neither, blanket ignore
     55             # crbug.com/355106: unknown, possibly related to crbug.com/290923
     56             ('^lumpy|^parrot', '', 'PM1_STS: WAK PWRBTN'),
     57         ]
     58     S0_WHITELIST = [  # (<board>, <kernel wake source>)
     59             # crbug.com/290923: spurious keyboard IRQ, believed to be from Servo
     60             ('^x86-alex|^lumpy|^parrot|^butterfly', 'serio0'),
     61         ]
     62 
     63 class MemoryError(SuspendFailure):
     64     """memory_suspend_test found memory corruption."""
     65     pass
     66 
     67 
     68 class SuspendNotAllowed(SuspendFailure):
     69     """Suspend was not allowed to be performed."""
     70     pass
     71 
     72 
     73 def prepare_wakeup(seconds):
     74     """Prepare the device to wake up from an upcoming suspend.
     75 
     76     @param seconds: The number of seconds to allow the device to suspend.
     77     """
     78     # May cause DUT not wake from sleep if the suspend time is 1 second.
     79     # It happens when the current clock (floating point) is close to the
     80     # next integer, as the RTC sysfs interface only accepts integers.
     81     # Make sure it is larger than or equal to 2.
     82     assert seconds >= 2
     83     wakeup_count = read_wakeup_count()
     84     alarm = int(rtc.get_seconds() + seconds)
     85     logging.debug('Suspend for %d seconds, wakealarm = %d', seconds, alarm)
     86     rtc.set_wake_alarm(alarm)
     87     return (alarm, wakeup_count)
     88 
     89 
     90 def check_wakeup(alarm):
     91     """Verify that the device did not wakeup early.
     92 
     93     @param alarm: The time at which the device was expected to wake up.
     94     """
     95     now = rtc.get_seconds()
     96     if now < alarm:
     97         logging.error('Woke up early at %d', now)
     98         raise SpuriousWakeupError('Woke from suspend early')
     99 
    100 
    101 def do_suspend(suspend_seconds, delay_seconds=0):
    102     """Do a suspend using the power manager.
    103 
    104     Wait for |delay_seconds|, suspend the system to RAM (S3), waking up again
    105     after having been suspended for |suspend_seconds|, using the
    106     powerd_dbus_suspend program. Function will block until suspend/resume
    107     has completed or failed. Returns the wake alarm time from the RTC as epoch.
    108 
    109     @param suspend_seconds: Number of seconds to suspend the DUT.
    110     @param delay_seconds: Number of seconds wait before suspending the DUT.
    111 
    112     """
    113     alarm, wakeup_count = prepare_wakeup(suspend_seconds)
    114     upstart.ensure_running('powerd')
    115     command = ('/usr/bin/powerd_dbus_suspend --delay=%d --timeout=30 '
    116                '--wakeup_count=%d') % (delay_seconds, wakeup_count)
    117     logging.info("Running '%s'", command)
    118     os.system(command)
    119     check_wakeup(alarm)
    120     return alarm
    121 
    122 
    123 def suspend_bg_for_dark_resume(delay_seconds=0):
    124     """Do a non-blocking indefinite suspend using power manager. ONLY USE THIS
    125     IF YOU ARE ABSOLUTELY CERTAIN YOU NEED TO.
    126 
    127     Wait for |delay_seconds|, then suspend to RAM (S3). This does not set an RTC
    128     alarm and does not pass an external wakeup count. It is meant to be used for
    129     dark resume testing, where the server-side API exposes it in such a fashion
    130     that the DUT will be woken by the server no matter how the test is exited.
    131 
    132     @param delay_seconds: Number of seconds wait before suspending the DUT.
    133 
    134     """
    135     upstart.ensure_running('powerd')
    136     command = ('/usr/bin/powerd_dbus_suspend --delay=%d '
    137                '--timeout=30') % delay_seconds
    138     logging.info("Running '%s'", command)
    139     process = multiprocessing.Process(target=os.system, args=(command,))
    140     process.start()
    141 
    142 
    143 def kernel_suspend(seconds):
    144     """Do a kernel suspend.
    145 
    146     Suspend the system to RAM (S3), waking up again after |seconds|, by directly
    147     writing to /sys/power/state. Function will block until suspend/resume has
    148     completed or failed.
    149 
    150     @param seconds: The number of seconds to suspend the device.
    151     """
    152     alarm, wakeup_count = prepare_wakeup(seconds)
    153     logging.debug('Saving wakeup count: %d', wakeup_count)
    154     write_wakeup_count(wakeup_count)
    155     try:
    156         logging.info('Suspending at %d', rtc.get_seconds())
    157         with open(SYSFS_POWER_STATE, 'w') as sysfs_file:
    158             sysfs_file.write('mem')
    159     except IOError as e:
    160         logging.exception('Writing to %s failed', SYSFS_POWER_STATE)
    161         if e.errno == errno.EBUSY and rtc.get_seconds() >= alarm:
    162             # The kernel returns EBUSY if it has to abort because
    163             # another wakeup fires before we've reached suspend.
    164             raise SpuriousWakeupError('Received spurious wakeup in kernel.')
    165         else:
    166             # Some driver probably failed to suspend properly.
    167             # A hint as to what failed is in errno.
    168             raise KernelError('Suspend failed: %s' % e.strerror)
    169     else:
    170         logging.info('Woke from suspend at %d', rtc.get_seconds())
    171     logging.debug('New wakeup count: %d', read_wakeup_count())
    172     check_wakeup(alarm)
    173 
    174 
    175 def idle_suspend(seconds):
    176     """
    177     Wait for the system to suspend to RAM (S3), scheduling the RTC to wake up
    178     |seconds| after this function was called. Caller must ensure that the system
    179     will idle-suspend in time for this to happen. Returns the wake alarm time
    180     from the RTC as epoch.
    181 
    182     @param seconds: The number of seconds before wakeup.
    183     """
    184     alarm, _ = prepare_wakeup(seconds)
    185     while rtc.get_seconds() < alarm:
    186         time.sleep(0.2)
    187 
    188     # tell powerd something happened, or it will immediately idle-suspend again
    189     # TODO: switch to cros.power_utils#call_powerd_dbus_method once this
    190     # horrible mess with the WiFi tests and this file's imports is solved
    191     logging.debug('Simulating user activity after idle suspend...')
    192     os.system('dbus-send --type=method_call --system '
    193               '--dest=org.chromium.PowerManager /org/chromium/PowerManager '
    194               'org.chromium.PowerManager.HandleUserActivity')
    195 
    196     return alarm
    197 
    198 
    199 def memory_suspend(seconds, size=0):
    200     """Do a memory suspend.
    201 
    202     Suspend the system to RAM (S3), waking up again after |seconds|, using
    203     the memory_suspend_test tool. Function will block until suspend/resume has
    204     completed or failed. Returns the wake alarm time from the RTC as epoch.
    205 
    206     @param seconds: The number of seconds to suspend the device.
    207     @param size: Amount of memory to allocate, in bytes. 
    208                  Set to 0 to let memory_suspend_test determine amount of memory.   
    209     """
    210     # since we cannot have utils.system_output in here, we need a workaround
    211     output = '/tmp/memory_suspend_output'
    212     alarm, wakeup_count = prepare_wakeup(seconds)
    213     status = os.system('/usr/bin/memory_suspend_test --wakeup_count=%d '
    214                        '--size=%d > %s' % (wakeup_count, size, output))
    215     status = os.WEXITSTATUS(status)
    216     if status == 2:
    217         logging.error('memory_suspend_test found the following errors:')
    218         for line in open(output, 'r'):
    219             logging.error(line)
    220         raise MemoryError('Memory corruption found after resume')
    221     elif status == 1:
    222         raise SuspendFailure('Failure in powerd_suspend during memory test')
    223     elif status:
    224         raise SuspendFailure('Unknown failure in memory_suspend_test (crash?)')
    225     check_wakeup(alarm)
    226     return alarm
    227 
    228 
    229 def read_wakeup_count():
    230     """
    231     Retrieves the current value of /sys/power/wakeup_count.
    232     """
    233     with open(SYSFS_WAKEUP_COUNT) as sysfs_file:
    234         return int(sysfs_file.read().rstrip('\n'))
    235 
    236 
    237 def write_wakeup_count(wakeup_count):
    238     """Writes a value to /sys/power/wakeup_count.
    239 
    240     @param wakeup_count: The wakeup count value to write.
    241     """
    242     try:
    243         with open(SYSFS_WAKEUP_COUNT, 'w') as sysfs_file:
    244             sysfs_file.write(str(wakeup_count))
    245     except IOError as e:
    246         if (e.errno == errno.EINVAL and read_wakeup_count() != wakeup_count):
    247             raise SpuriousWakeupError('wakeup_count changed before suspend')
    248         else:
    249             raise
    250