• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7import errno
8import fcntl
9import logging
10import multiprocessing
11import os
12import time
13
14import common
15from autotest_lib.client.cros import rtc
16from autotest_lib.client.cros import upstart
17
18SYSFS_POWER_STATE = '/sys/power/state'
19SYSFS_WAKEUP_COUNT = '/sys/power/wakeup_count'
20
21PAUSE_ETHERNET_HOOK_FILE = '/run/autotest_pause_ethernet_hook'
22pause_ethernet_file = None
23
24
25class SuspendFailure(Exception):
26    """Base class for a failure during a single suspend/resume cycle."""
27    pass
28
29
30class SuspendTimeout(SuspendFailure):
31    """Suspend took too long, got wakeup event (RTC tick) before it was done."""
32    pass
33
34
35class KernelError(SuspendFailure):
36    """Kernel problem encountered during suspend/resume.
37
38    Allowlist is an array of a 2-entry tuples consisting of (source regexp, text
39    regexp).  For example, kernel output might look like,
40
41      [  597.079950] WARNING: CPU: 0 PID: 21415 at \
42      <path>/v3.18/drivers/gpu/drm/i915/intel_pm.c:3687 \
43      skl_update_other_pipe_wm+0x136/0x18a()
44      [  597.079962] WARN_ON(!wm_changed)
45
46    source regexp should match first line above while text regexp can match
47    up to 2 lines below the source.  Note timestamps are stripped prior to
48    comparing regexp.
49    """
50    ALLOWLIST = [
51        # crosbug.com/37594: debug tracing clock desync we don't care about
52        (r'kernel/trace/ring_buffer.c:\d+ rb_reserve_next_event',
53         r'Delta way too big!'),
54        # TODO(crosbug.com/p/52008): Remove from allowlist once watermark
55        # implementation has landed.
56        (r'v3.18/\S+/intel_pm.c:\d+ skl_update_other_pipe_wm',
57         r'WARN_ON\(\!wm_changed\)')
58    ]
59
60
61class FirmwareError(SuspendFailure):
62    """String 'ERROR' found in firmware log after resume."""
63    ALLOWLIST = [
64        # crosbug.com/36762: no one knows, but it has always been there
65        ('^stumpy', r'PNP: 002e\.4 70 irq size: 0x0000000001 not assigned'),
66        # crbug.com/221538: no one knows what ME errors mean anyway
67        ('^parrot', r'ME failed to respond'),
68        # b/64684441: eve SKU without eMMC
69        ('^eve', r'Card did not respond to voltage select!'),
70    ]
71
72
73class SpuriousWakeupError(SuspendFailure):
74    """Received spurious wakeup while suspending or woke before schedule."""
75    S3_ALLOWLIST = [  # (<board>, <eventlog wake source>, <syslog wake source>)
76        # crbug.com/220014: spurious trackpad IRQs
77        ('^link', 'Wake Source | GPIO | 12', ''),
78        # crbug.com/345327: unknown, probably same as crbug.com/290923
79        ('^x86-alex', '', ''),   # alex can report neither, blanket ignore
80        # crbug.com/355106: unknown, possibly related to crbug.com/290923
81        ('^lumpy|^parrot', '', 'PM1_STS: WAK PWRBTN'),
82    ]
83    S0_ALLOWLIST = [  # (<board>, <kernel wake source>)
84        # crbug.com/290923: spurious keyboard IRQ, believed to be from Servo
85        ('^x86-alex|^lumpy|^parrot|^butterfly', 'serio0'),
86        # crosbug.com/p/46140: battery event caused by MKBP
87        ('^elm|^oak|^hana', 'spi32766.0'),
88    ]
89
90
91class MemoryError(SuspendFailure):
92    """memory_suspend_test found memory corruption."""
93    pass
94
95
96class SuspendNotAllowed(SuspendFailure):
97    """Suspend was not allowed to be performed."""
98    pass
99
100
101class S0ixResidencyNotChanged(SuspendFailure):
102    """power_SuspendStress test found CPU/SoC is unable to idle properly
103    when suspended to S0ix. """
104    pass
105
106
107class S2IdleResidencyNotChanged(SuspendFailure):
108    """power_SuspendStress test found CPU/SoC is unable to idle properly
109    when suspended to s2idle. """
110    pass
111
112
113def prepare_wakeup(seconds):
114    """Prepare the device to wake up from an upcoming suspend.
115
116    @param seconds: The number of seconds to allow the device to suspend.
117    """
118    # May cause DUT not wake from sleep if the suspend time is 1 second.
119    # It happens when the current clock (floating point) is close to the
120    # next integer, as the RTC sysfs interface only accepts integers.
121    # Make sure it is larger than or equal to 2.
122    assert seconds >= 2
123    wakeup_count = read_wakeup_count()
124    estimated_alarm = int(rtc.get_seconds() + seconds)
125    logging.debug('Suspend for %d seconds, estimated wakealarm = %d',
126                  seconds, estimated_alarm)
127    return (estimated_alarm, wakeup_count)
128
129
130def check_wakeup(estimated_alarm):
131    """Verify that the device did not wakeup early.
132
133    @param estimated_alarm: The lower bound time at which the device was
134                            expected to wake up.
135    """
136    now = rtc.get_seconds()
137    if now < estimated_alarm:
138        logging.error('Woke up %d secs early', (estimated_alarm - now))
139        raise SpuriousWakeupError('Woke from suspend early')
140
141
142def pause_check_network_hook():
143    """Stop check_ethernet.hook from running.
144
145    Lock will be held until test exits, or resume_check_network_hook() is
146    called. check_ethernet.hook may also "break" the lock if it judges you've
147    held it too long.
148
149    Can be called multiple times, to refresh the lock.
150    """
151    # If this function is called multiple times, we intentionally re-assign
152    # this global, which closes out the old lock and grabs it anew.
153    # We intentionally "touch" the file to update its mtime, so we can judge
154    # how long locks are held.
155    global pause_ethernet_file
156    pause_ethernet_file = open(PAUSE_ETHERNET_HOOK_FILE, 'w+')
157    try:
158        # This is a blocking call unless an error occurs.
159        fcntl.flock(pause_ethernet_file, fcntl.LOCK_EX)
160    except IOError:
161        pass
162
163
164def resume_check_network_hook():
165    """Resume check_ethernet.hook.
166
167    Must call pause_check_network_hook() at least once before calling this.
168    """
169    global pause_ethernet_file
170    # Closing the file descriptor releases the lock.
171    pause_ethernet_file.close()
172    pause_ethernet_file = None
173
174
175def do_suspend(wakeup_timeout, delay_seconds=0):
176    """Suspend using the power manager with a wakeup timeout.
177
178    Wait for |delay_seconds|, suspend the system(S3/S0iX) using
179    powerd_dbus_suspend program. powerd_dbus_suspend first sets a wakeup alarm
180    on the dut for |current_time + wakeup_timeout|. Thus the device tries to
181    resume at |current_time + wakeup_timeout| irrespective of when it suspended.
182    This means that RTC can trigger an early resume and prevent suspend.
183
184    Function will block until suspend/resume has completed or failed.
185    Returns the wake alarm time from the RTC as epoch.
186
187    @param wakeup_timeout: time from now after which the device has to.
188    @param delay_seconds: Number of seconds wait before suspending the DUT.
189
190    """
191    pause_check_network_hook()
192    estimated_alarm, wakeup_count = prepare_wakeup(wakeup_timeout)
193    upstart.ensure_running('powerd')
194    command = ('/usr/bin/powerd_dbus_suspend --delay=%d --timeout=30 '
195               '--wakeup_count=%d --wakeup_timeout=%d' %
196               (delay_seconds, wakeup_count, wakeup_timeout))
197    logging.info("Running '%s'", command)
198    os.system(command)
199    check_wakeup(estimated_alarm)
200    return estimated_alarm
201
202
203def suspend_for(time_in_suspend, delay_seconds=0):
204    """Suspend using the power manager and spend |time_in_suspend| in suspend.
205
206    Wait for |delay_seconds|, suspend the system(S3/S0iX) using
207    powerd_dbus_suspend program. power manager sets a wakeup alarm on the dut
208    for |time_in_suspend| just before suspending
209    (writing power state to /sys/power/state). Thus the device spends most of
210    |time_in_suspend| in S0iX/S3.
211
212    Function will block until suspend/resume has completed or failed.
213    Returns the wake alarm time from the RTC as epoch.
214
215    @param time_in_suspend: Number of seconds to suspend the DUT.
216    @param delay_seconds: Number of seconds wait before suspending the DUT.
217    """
218    pause_check_network_hook()
219
220    wakeup_count = read_wakeup_count()
221    upstart.ensure_running('powerd')
222    command = ('/usr/bin/powerd_dbus_suspend --delay=%d --timeout=30 '
223               '--wakeup_count=%d --suspend_for_sec=%d' %
224               (delay_seconds, wakeup_count, time_in_suspend))
225    logging.info("Running '%s'", command)
226    os.system(command)
227
228
229def suspend_bg_for_dark_resume(suspend_seconds, delay_seconds=0):
230    """Do a non-blocking suspend using power manager.
231
232    Wait for |delay_seconds|, then suspend with an rtc alarm for
233    waking up again after having been suspended for |suspend_seconds|, using
234    the powerd_dbus_suspend program all in the background.
235
236    @param suspend_seconds: Number of seconds to suspend the DUT.
237    @param delay_seconds: Number of seconds wait before suspending the DUT.
238
239    """
240    upstart.ensure_running('powerd')
241    # Disarm any existing wake alarms so as to prevent early wakeups.
242    os.system('echo 0 > /sys/class/rtc/rtc0/wakealarm')
243    wakeup_count = read_wakeup_count()
244    command = ('/usr/bin/powerd_dbus_suspend --delay=%d --timeout=30 '
245               '--wakeup_count=%d --wakeup_timeout=%d '
246               '--disable_dark_resume=false' %
247               (delay_seconds, wakeup_count, suspend_seconds))
248    logging.info("Running '%s'", command)
249    process = multiprocessing.Process(target=os.system, args=(command,))
250    process.start()
251
252
253def kernel_suspend(seconds, state='mem'):
254    """Do a kernel suspend.
255
256    Suspend the system to @state, waking up again after |seconds|, by directly
257    writing to /sys/power/state. Function will block until suspend/resume has
258    completed or failed.
259
260    @param seconds: The number of seconds to suspend the device.
261    @param state: power state to suspend to.  DEFAULT mem.
262    """
263    alarm, wakeup_count = prepare_wakeup(seconds)
264    rtc.set_wake_alarm(alarm)
265    logging.debug('Saving wakeup count: %d', wakeup_count)
266    write_wakeup_count(wakeup_count)
267    try:
268        logging.info('Suspending at %d', rtc.get_seconds())
269        with open(SYSFS_POWER_STATE, 'w') as sysfs_file:
270            sysfs_file.write(state)
271    except IOError as e:
272        logging.exception('Writing to %s failed', SYSFS_POWER_STATE)
273        if e.errno == errno.EBUSY and rtc.get_seconds() >= alarm:
274            # The kernel returns EBUSY if it has to abort because
275            # another wakeup fires before we've reached suspend.
276            raise SpuriousWakeupError('Received spurious wakeup in kernel.')
277        else:
278            # Some driver probably failed to suspend properly.
279            # A hint as to what failed is in errno.
280            raise KernelError('Suspend failed: %s' % e.strerror)
281    else:
282        logging.info('Woke from suspend at %d', rtc.get_seconds())
283    logging.debug('New wakeup count: %d', read_wakeup_count())
284    check_wakeup(alarm)
285
286
287def idle_suspend(seconds):
288    """
289    Wait for the system to suspend to RAM (S3), scheduling the RTC to wake up
290    |seconds| after this function was called. Caller must ensure that the system
291    will idle-suspend in time for this to happen. Returns the wake alarm time
292    from the RTC as epoch.
293
294    @param seconds: The number of seconds before wakeup.
295    """
296    alarm, _ = prepare_wakeup(seconds)
297    rtc.set_wake_alarm(alarm)
298    while rtc.get_seconds() < alarm:
299        time.sleep(0.2)
300
301    # tell powerd something happened, or it will immediately idle-suspend again
302    # TODO: switch to cros.power_utils#call_powerd_dbus_method once this
303    # horrible mess with the WiFi tests and this file's imports is solved
304    logging.debug('Simulating user activity after idle suspend...')
305    os.system('dbus-send --type=method_call --system '
306              '--dest=org.chromium.PowerManager /org/chromium/PowerManager '
307              'org.chromium.PowerManager.HandleUserActivity')
308
309    return alarm
310
311
312def memory_suspend(seconds, size=0):
313    """Do a memory suspend.
314
315    Suspend the system to RAM (S3), waking up again after |seconds|, using
316    the memory_suspend_test tool. Function will block until suspend/resume has
317    completed or failed.
318
319    @param seconds: The number of seconds to suspend the device.
320    @param size: Amount of memory to allocate, in bytes.
321                 Set to 0 to let memory_suspend_test determine amount of memory.
322    """
323    # since we cannot have utils.system_output in here, we need a workaround
324    output = '/tmp/memory_suspend_output'
325    wakeup_count = read_wakeup_count()
326    status = os.system('/usr/bin/memory_suspend_test --wakeup_count=%d '
327                       '--size=%d > %s --suspend_for_sec=%d' %
328                       (wakeup_count, size, output, seconds))
329    status = os.WEXITSTATUS(status)
330    if status == 2:
331        logging.error('memory_suspend_test found the following errors:')
332        for line in open(output, 'r'):
333            logging.error(line)
334        raise MemoryError('Memory corruption found after resume')
335    elif status == 1:
336        raise SuspendFailure('Failure in powerd_suspend during memory test')
337    elif status:
338        raise SuspendFailure('Unknown failure in memory_suspend_test (crash?)')
339
340
341def read_wakeup_count():
342    """
343    Retrieves the current value of /sys/power/wakeup_count.
344    """
345    with open(SYSFS_WAKEUP_COUNT) as sysfs_file:
346        return int(sysfs_file.read().rstrip('\n'))
347
348
349def write_wakeup_count(wakeup_count):
350    """Writes a value to /sys/power/wakeup_count.
351
352    @param wakeup_count: The wakeup count value to write.
353    """
354    try:
355        with open(SYSFS_WAKEUP_COUNT, 'w') as sysfs_file:
356            sysfs_file.write(str(wakeup_count))
357    except IOError as e:
358        if (e.errno == errno.EINVAL and read_wakeup_count() != wakeup_count):
359            raise SpuriousWakeupError('wakeup_count changed before suspend')
360        else:
361            raise
362