| #!/bin/sh |
| |
| # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # This script is responsible for suspending and resuming the system. It is run |
| # as root by powerd_setuid_helper, which is run by powerd. |
| |
| . /usr/share/misc/shflags |
| |
| DEFINE_integer wakeup_count -1 \ |
| "wakeup event count from start of suspend. -1 to disable" w |
| DEFINE_integer suspend_duration -1 \ |
| "indicates that the system should wake up after this many seconds" d |
| DEFINE_boolean suspend_to_idle false \ |
| "indicates that the system should suspend to idle (freeze)" i |
| |
| # Exit codes returned by this script and reported via the |
| # Power.SuspendResult histogram: |
| |
| # The system successfully suspended and resumed. |
| readonly RESULT_SUCCESS=0 |
| |
| # A suspend failure occurred in the kernel after writing to /sys/power/state |
| # (i.e. the write to /sys/power/state failed with a non-EBUSY reason). |
| readonly RESULT_FAILURE=1 |
| |
| # The suspend attempt was canceled as the result of a wakeup event occurring |
| # between powerd's read from /sys/power/wakeup_count and this script's write to |
| # it (i.e. the write to wakeup_count failed). |
| readonly RESULT_CANCELED_EARLY=2 |
| |
| # The suspend attempt was canceled as the result of a wakeup event occurring |
| # between the write to /sys/power/wakeup_count and the write to |
| # /sys/power/state (i.e. the write to /sys/power/state failed with EBUSY). |
| readonly RESULT_CANCELED_LATE=3 |
| |
| # Histogram-only max value. |
| readonly RESULT_MAX=4 |
| |
| # Directory where this script and send_metrics_on_resume (running as root) |
| # write temporary state files. |
| readonly ROOT_RUN_DIR=/run/power_manager/root |
| |
| # For testing: write RTC resume timestamp to this file if it exists |
| readonly TIMESTAMP_FILE="${ROOT_RUN_DIR}"/hwclock-on-resume |
| |
| # File used to record the power status (AC vs. battery) before suspending. Read |
| # by send_metrics_on_resume (but only when run after resuming; this doesn't |
| # need to persist across reboots). |
| readonly POWER_STATUS_ON_SUSPEND_FILE="${ROOT_RUN_DIR}"/power-status-on-suspend |
| |
| # File containing resume-related timing information. Written by |
| # send_metrics_on_resume and read by autotests; doesn't need to persist |
| # across reboots. |
| readonly LAST_RESUME_TIMINGS_FILE="${ROOT_RUN_DIR}"/last_resume_timings |
| |
| # Directory where this script (running as root) writes files that must |
| # persist across reboots. |
| readonly ROOT_SPOOL_DIR=/var/spool/power_manager/root |
| |
| # File containing the time at which we started suspending. Read by |
| # send_metrics_on_resume. |
| readonly SUSPEND_TO_RAM_TIME_FILE="${ROOT_SPOOL_DIR}"/suspend-to-ram-time |
| |
| # File containing suspend-related information to log on failure. |
| readonly SUSPEND_STATS_FILE=/sys/kernel/debug/suspend_stats |
| |
| # Compact if order 3 page count is less than this. |
| readonly COMPACT_ORDER3_THRESHOLD=500 |
| |
| # For testing: do not compact before suspending if this file exists. |
| readonly NO_COMPACT_FILE="${ROOT_RUN_DIR}"/no-compact |
| |
| log_msg() { |
| logger -t "powerd_suspend[$$]" -- "$@" |
| } |
| |
| log_msg_from_file() { |
| logger -t "powerd_suspend[$$]" -f "$1" |
| } |
| |
| log_wakeup_count() { |
| log_msg "wakeup_count is $(cat /sys/power/wakeup_count)" |
| } |
| |
| log_suspend_stats() { |
| log_msg "--- begin ${SUSPEND_STATS_FILE} ---" |
| local tempfile="$(mktemp --tmpdir powerd_suspend_stats.XXXXXXXXXX)" |
| # Omit blank lines and replace tabs with spaces so syslog won't escape them |
| # to "#011". |
| grep -e '\S' "${SUSPEND_STATS_FILE}" | tr '\t' ' ' >"${tempfile}" |
| log_msg_from_file "${tempfile}" |
| log_msg "--- end ${SUSPEND_STATS_FILE} ---" |
| rm "${tempfile}" |
| } |
| |
| enable_debug() { |
| # Enable debug info. |
| if [ -e /sys/module/printk/parameters/console_suspend ]; then |
| if [ "${FLAGS_suspend_to_idle}" -eq 0 ]; then |
| # Developers can change this to 'N' if they want to temporarily not |
| # disable the console while using S0ix. Note that this comes with the |
| # risk of failure to enter S0ix due to console activity on the UART. |
| log_msg "Suspending console since S0ix can fail if console not suspended" |
| printf 'Y' > /sys/module/printk/parameters/console_suspend |
| else |
| printf 'N' > /sys/module/printk/parameters/console_suspend |
| fi |
| fi |
| } |
| |
| # Checks if the system has a Novatel Wireless Gobi 3000 E396/E396U modem. |
| has_gobi_modem() { |
| lsusb -d 1410:a021 || lsusb -d 1410:a023 |
| } |
| |
| # Blaze modems need to be powered off (MODEM_EN low) when suspending and |
| # powered back on (MODEM_EN high) when resuming. |
| |
| # Return true on Blaze systems. |
| is_blaze() { |
| crossystem hwid | grep -q 'BLAZE' |
| } |
| |
| is_kip() { |
| crossystem hwid | grep -q '^KIP ' |
| } |
| |
| # Checks if the system has a Huawei Technologies Mobile Broadband Module. |
| has_huawei_modem() { |
| lsusb -d 12d1:15bb |
| } |
| |
| # Powers "on" or "off" the built-in modem on Blaze by driving the MODEM_EN |
| # GPIO. |
| power_blaze_modem() { |
| local op="$1" |
| local gpioval |
| |
| case "${op}" in |
| on) |
| gpioval=1 |
| ;; |
| off) |
| gpioval=0 |
| ;; |
| *) |
| log_msg "Invalid operation 'power_blaze_modem ${op}'" |
| return 1 |
| ;; |
| esac |
| |
| cd /sys/class/gpio |
| if [ ! -d gpio148 ]; then |
| echo 148 > export |
| fi |
| echo out > gpio148/direction |
| echo "${gpioval}" > gpio148/value |
| |
| if [ $? -eq 0 ]; then |
| log_msg "Power ${op} built-in modem" |
| return 0 |
| else |
| log_msg "Failed to power ${op} built-in modem" |
| return 1 |
| fi |
| } |
| |
| # Powers "on" or "off" the built-in modem via EC. |
| # |
| # This function uses "ectool", provided by chromeos-base/ec-utils, to |
| # communicate with EC. chromeos-base/power_manager only depends on |
| # chromeos-base/ec-utils on a per-board basis. Before using this function for a |
| # particular board, update the chromeos-base/power_manager ebuild to make sure |
| # the chromeos-base/ec-utils dependency is specified for that board. |
| ec_power_modem() { |
| local op="$1" |
| |
| case "${op}" in |
| on) |
| ectool wireless 0x4 0x4 |
| ;; |
| off) |
| ectool wireless 0x0 0x4 |
| ;; |
| *) |
| log_msg "Invalid operation 'ec_power_modem ${op}'" |
| return 1 |
| ;; |
| esac |
| |
| if [ $? -eq 0 ]; then |
| log_msg "Power ${op} built-in modem" |
| return 0 |
| else |
| log_msg "Failed to power ${op} built-in modem" |
| return 1 |
| fi |
| } |
| |
| # Define the white list of the bt_vendor:bt_product that allow to set event |
| # mask where bt_vendor:* means all products of the vendor are allowed. |
| # TODO: change the whitelist to blacklist after verifying all existing |
| # controllers. Only a small portion of controllers do not support |
| # setting the event mask, e.g., the Mode Change event. |
| # Refer to the following file for the list of bluetooth controllers. |
| # src/platform2/power_manager/udev/gen_autosuspend_rules.py |
| BT_MASK_WHITELIST="8087:*" |
| |
| # Check if bt_vendor:bt_product is in the white list. |
| in_bt_mask_whitelist() { |
| local vendor="$1" |
| local product="$2" |
| [ "${BT_MASK_WHITELIST#*${vendor}:\*}" != "${BT_MASK_WHITELIST}" -o \ |
| "${BT_MASK_WHITELIST#*${vendor}:${product}}" != "${BT_MASK_WHITELIST}" ] |
| } |
| |
| to_hex() { |
| echo "obase=16; $(($1))" | bc |
| } |
| |
| # Enable or disable some Bluetooth events based on the "enable" or |
| # "disable" argument. |
| # Different Bluetooth devices and the Bluetooth controllers of |
| # different vendors may send different events to the host during |
| # system suspension which would wake up the system spuriously. |
| # Disabling the events on suspension would help prevent the system |
| # from waking up spuriously. |
| # Refer to Bluetooth Spec 4.2 Vol 2 Part E 7.3.1 Set Event Mask Command |
| # about the event mask values. |
| bt_hci_events_ctrl() { |
| local file |
| for file in /sys/class/bluetooth/hci[0-9]; do |
| local hcidev="$(basename "${file}")" |
| local bt_vendor="" |
| local retval="" |
| local usb_path="$(readlink ${file})" |
| usb_path="/sys/class/bluetooth/${usb_path%/bluetooth/*}/.." |
| bt_vendor="$(cat "${usb_path}"/idVendor)" |
| bt_product="$(cat "${usb_path}"/idProduct)" |
| |
| # Don't do anything for controllers not in BT_MASK_WHITELIST. |
| # Some of them may not support it well (See http://crbug.com/803272) |
| if ! in_bt_mask_whitelist "${bt_vendor}" "${bt_product}"; then |
| log_msg "Skipping $1 BT HCI events on" "${bt_vendor}:${bt_product}" |
| continue |
| fi |
| if [ "$1" = "disable" ]; then |
| log_msg "Disabling some Bluetooth HCI events before suspend" |
| # Events turned off during system suspension: |
| # Mode Change Event: 0x0000000000080000 |
| # Sniff Subrating Event: 0x0000200000000000 |
| # Max Slots Change Event: 0x0000000004000000 |
| # Note: |
| # a. We have to be careful when disabling mask values to avoid |
| # the system from going out of sync with the controller on resume. |
| # - Mode Change Event and Sniff Subrating Event: these states will be |
| # synced again after the host sends Exit Sniff mode command below. |
| # - Max Slots Change Event: kernel does not care this event at all. |
| # b. The following mask values are coded in Little Endian order. |
| # TODO: fix the terrible scary hack using hcitool. |
| # - Should recover the original mask values instead of all on |
| # after resume. |
| # - It should be implemented in Bluetooth stack and be called by |
| # dbus. |
| timeout -k 2 2 hcitool -i "${hcidev}" cmd 03 01 ff ff f7 fb ff df ff ff |
| elif [ "$1" = "enable" ]; then |
| log_msg "Enabling Bluetooth HCI events after resume" |
| timeout -k 2 2 hcitool -i "${hcidev}" cmd 03 01 ff ff ff ff ff ff ff ff |
| |
| # Exit Sniff mode for all ACL connections. |
| # The command of "hcitool con" shows something like |
| # Connections: |
| # > ACL 04:52:C7:83:95:10 handle 256 state 1 lm MASTER AUTH ENCRYPT |
| # > ACL 04:52:C7:C3:65:B5 handle 512 state 1 lm MASTER AUTH ENCRYPT |
| handles="$(echo "$(hcitool con)" | awk '/ACL/ {print $5}')" |
| for handle in $handles; do |
| lo_byte="$(to_hex $((handle % 256)))" |
| hi_byte="$(to_hex $((handle / 256)))" |
| log_msg "Exiting Sniff mode for connection handle ${lo_byte} ${hi_byte}" |
| timeout -k 2 2 hcitool -i "${hcidev}" cmd 02 04 "${lo_byte}" \ |
| "${hi_byte}" |
| done |
| fi |
| retval=$? |
| # retval = 124/137 indicates timedout, please refer to timeout man page |
| if [ ${retval} -eq 137 ] || [ ${retval} -eq 124 ]; then |
| log_msg "hcitool command timed out (Killed)" |
| elif [ ${retval} -ne 0 ]; then |
| log_msg "hcitool failed with exit status ${retval}" |
| fi |
| done |
| } |
| |
| # Writes the wakeup count that was passed to this script to |
| # /sys/power/wakeup_count. Returns success if the write fails, indicating |
| # that one or more wakeup events occurred in the meantime. |
| saw_wakeup_event() { |
| if [ "${FLAGS_wakeup_count}" -eq -1 ] || |
| echo "${FLAGS_wakeup_count}" > /sys/power/wakeup_count; then |
| return 1 |
| else |
| log_msg "Aborting suspend, wake event received" |
| log_wakeup_count |
| return 0 |
| fi |
| } |
| |
| |
| # Sends suspend result (one of the above RESULT_* values) to UMA. |
| send_uma_result() { |
| metrics_client -e Power.SuspendResult "$1" "${RESULT_MAX}" & |
| } |
| |
| setup_dark_resume() { |
| if [ "${FLAGS_suspend_duration}" -ne -1 ]; then |
| echo 0 > /sys/class/rtc/rtc0/wakealarm |
| echo "+${FLAGS_suspend_duration}" > /sys/class/rtc/rtc0/wakealarm |
| fi |
| } |
| |
| # Gets the allocatable order 3 page count. Using order 3 threshold because |
| # there are many order 3 allocation by network modules when running low memory |
| # suspending stress test. /proc/pagetypeinfo provides available high order |
| # pages count, example: |
| # Free pages count per migrate type at order 0 1 2 3 4 ... 10 |
| # ... |
| # Node 0, zone DMA32, type Unmovable 1 9 29 5 2 ... 0 |
| # Node 0, zone DMA32, type Reclaimable 160 204 17 8 0 ... 0 |
| # Node 0, zone DMA32, type Movable 0 1163 1530 1236 298 ... 0 |
| # Node 0, zone DMA32, type Reserve 0 0 0 0 0 ... 2 |
| # Movable order 3 field 1236: there are 1236 order 3 pages ((2 ^ 3) * 4KB |
| # consecutive memory). |
| order3_available() { |
| awk '(NF == 17) && |
| ($6 == "Unmovable" || $6 == "Movable" || $6 == "Reclaimable") { |
| size = 1 |
| for (i = 10; i <= 17; i++) { |
| sum += $i * size |
| size *= 2 |
| } |
| } |
| END { print sum }' /proc/pagetypeinfo |
| } |
| |
| # When system is suspending, memory compaction and swap are disabled. If |
| # memory is fragmented at this time, high order allocation would fail. |
| # When memory fragmentation is detected (too few allocatable order 3 pages), |
| # compact memory before suspending. For 4.4 or earlier kernel without |
| # kcompactd. |
| # TODO(crbug.com/827053): Check if compaction before suspending is necessary |
| # on newer kernel. |
| try_compact_memory() { |
| local order3="$(order3_available)" |
| log_msg "Available order 3 pages: ${order3}" |
| if uname -r | grep -q "^3\.\|^4\.4\." && [ ! -f ${NO_COMPACT_FILE} ] && \ |
| [ "${order3}" -le "${COMPACT_ORDER3_THRESHOLD}" ]; then |
| log_msg "Compacting memory before suspend" |
| echo 1 > /proc/sys/vm/compact_memory |
| log_msg "Available order 3 pages after compaction: $(order3_available)" |
| fi |
| } |
| |
| # Encourage people to use powerd_dbus_suspend instead of running this |
| # script directly so that e.g. suspend delays will happen. |
| if [ -z "${POWERD_SETUID_HELPER}" ]; then |
| echo "This script is called by powerd. Please run powerd_dbus_suspend" 1>&2 |
| echo "to manually exercise the full suspend path." 1>&2 |
| exit 1 |
| fi |
| |
| # Note: Don't change or remove this line without also updating |
| # send_metrics_on_resume. |
| log_msg "Going to suspend-to-RAM state: args=$@" |
| |
| # Parse command line. |
| FLAGS "$@" || exit 1 |
| eval set -- "${FLAGS_ARGV}" |
| |
| # The metrics library requires a max value of 2 rather than 1 |
| # (http://crbug.com/338015). |
| metrics_client -e Power.SuspendAttempt 0 2 & |
| |
| # Log the time before going to suspend (no-op if no RTC). |
| cp /sys/class/rtc/rtc0/since_epoch "${SUSPEND_TO_RAM_TIME_FILE}" \ |
| 2> /dev/null || true |
| |
| # Remove last_resume_timings to ensure the file is fresh on resume. |
| rm -f "${LAST_RESUME_TIMINGS_FILE}" |
| |
| # Store the current power status. |
| power_supply_info 2> /dev/null \ |
| | grep -Eq '^[[:space:]]+online:[[:space:]]+no$' |
| if [ $? -eq 0 ]; then |
| echo OnBattery > "${POWER_STATUS_ON_SUSPEND_FILE}" |
| else |
| echo OnAC > "${POWER_STATUS_ON_SUSPEND_FILE}" |
| fi |
| |
| # Is this a Blaze system with Huawei modem? |
| blaze_with_huawei_modem=0 |
| if is_blaze && has_huawei_modem; then |
| log_msg "Blaze with Huawei modem" |
| blaze_with_huawei_modem=1 |
| fi |
| |
| # Is this a Kip system with Huawei modem? |
| kip_with_huawei_modem=0 |
| if is_kip && has_huawei_modem; then |
| log_msg "Kip with Huawei modem" |
| kip_with_huawei_modem=1 |
| fi |
| |
| enable_debug |
| |
| result="${RESULT_FAILURE}" |
| |
| if saw_wakeup_event; then |
| # Note: The client/cros/power_suspend.py module in autotest depends on |
| # this message. |
| log_msg "Cancel suspend at kernel" |
| |
| # This file is usually removed by send_metrics_on_resume, but that script |
| # isn't run if the suspend attempt is unsuccessful. |
| rm -f "${SUSPEND_TO_RAM_TIME_FILE}" |
| |
| result="${RESULT_CANCELED_EARLY}" |
| else |
| # Before suspending, power down this problematic modem. |
| if [ "${blaze_with_huawei_modem}" -eq 1 ]; then |
| power_blaze_modem off |
| fi |
| if [ "${kip_with_huawei_modem}" -eq 1 ]; then |
| ec_power_modem off |
| fi |
| |
| # Disable undesired events in the Bluetooth event mask. |
| bt_hci_events_ctrl "disable" |
| |
| # Compact memory if it is fragmented. |
| try_compact_memory |
| |
| log_msg "Finalizing suspend" |
| # Make sure that hwclock is cached for when we wake up, this way we don't |
| # wait for io and get a more accurate time for wakeup |
| # -V prints a little version information and exits, loading .so but not |
| # making actual RTC accesses. Do *NOT* change this to a normal execution... |
| # it will trigger an RTC interrupt that may count as a wakeup reason, abort |
| # your suspend and cause you hours of pain and confusion! |
| if [ -f "${TIMESTAMP_FILE}" ]; then |
| /sbin/hwclock -V > /dev/null |
| else |
| # We don't want to overwrite the alarm for whatever test might be |
| # running. |
| setup_dark_resume |
| fi |
| |
| # Echo freeze to /sys/power/state on platforms that support suspend to idle. |
| action='mem' |
| if [ "${FLAGS_suspend_to_idle}" -eq 0 ]; then |
| action='freeze' |
| fi |
| |
| # Suspend to RAM. This is piped through cat since we want to determine |
| # the causes of failures -- dash's "echo" command appears to not print |
| # anything in response to write errors. |
| error=$( (printf '%s' "${action}" | cat > /sys/power/state) 2>&1) |
| if [ $? -eq 0 ]; then |
| # On resume: |
| result="${RESULT_SUCCESS}" |
| |
| if [ -f "${TIMESTAMP_FILE}" ]; then |
| # Record the hwclock time at resume for the power_Resume test. |
| if /sbin/hwclock -r --utc > "${TIMESTAMP_FILE}" 2>&1; then |
| log_msg "Recorded hwclock time for test" |
| else |
| log_msg "/sbin/hwclock failed: $(cat "${TIMESTAMP_FILE}")" |
| log_msg "debug: $(/sbin/hwclock -r --debug --utc 2>&1)" |
| fi |
| fi |
| |
| if [ -e /sys/firmware/log ]; then |
| # Grep /sys/firmware/log for WAK. |
| # shellcheck disable=SC2002 |
| # cat is required because tac is unable to correctly processes |
| # /sys/firmware/log. tac reads in 8k chunks, but the kernel only returns |
| # 4k chunks. |
| wakeup_source="$(cat /sys/firmware/log | tac | \ |
| grep -m1 -A1 -B1 "PM1_STS: WAK" | tac)" |
| if [ -z "${wakeup_source}" ]; then |
| wakeup_source='unknown' |
| fi |
| log_msg "wake source: ${wakeup_source}" |
| fi |
| |
| # Send UMA metrics. |
| send_metrics_on_resume & |
| else |
| # The write will fail with EBUSY if additional wakeup events have |
| # occurred since the earlier write to /sys/power/wakeup_count. |
| if echo "${error}" | grep -q 'Device or resource busy'; then |
| log_msg "Warning: Device or resource busy on write to /sys/power/state" |
| log_suspend_stats |
| log_wakeup_count |
| result="${RESULT_CANCELED_LATE}" |
| else |
| # Note: The client/cros/power_suspend.py module in autotest depends on |
| # this message. |
| log_msg "Error writing to /sys/power/state: ${error}" |
| log_suspend_stats |
| result="${RESULT_FAILURE}" |
| fi |
| fi |
| |
| # Power back on the Huawei modem if it was powered off before suspend. |
| if [ "${blaze_with_huawei_modem}" -eq 1 ]; then |
| power_blaze_modem on |
| fi |
| if [ "${kip_with_huawei_modem}" -eq 1 ]; then |
| ec_power_modem on |
| fi |
| |
| # Enable all events in the Bluetooth event mask. |
| bt_hci_events_ctrl "enable" |
| fi |
| |
| send_uma_result "${result}" |
| |
| # Refresh mosys eventlog to help feedback reports pick up the latest snapshot. |
| /usr/share/userfeedback/scripts/eventlog & |
| |
| log_msg "Resume finished" |
| |
| return "${result}" |