]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sleep: allow HibernateDelaySec and low-battery hibernation to work together
authorgvenugo3 <gvenugo3@asu.edu>
Tue, 3 Feb 2026 03:57:30 +0000 (20:57 -0700)
committerLennart Poettering <lennart@poettering.net>
Thu, 5 Feb 2026 09:28:47 +0000 (10:28 +0100)
Previously, setting HibernateDelaySec= would disable ACPI battery trip
point (_BTP) alarms, forcing the system to rely solely on software
polling for battery checks. This could result in the battery draining
to 0% between polling intervals, causing data loss.

Now, when ACPI _BTP is available AND HibernateDelaySec= is set, both
mechanisms work together. The system will hibernate on whichever comes
first: low battery (instant hardware alarm) or the configured timeout.

This also properly respects HibernateOnACPower=no by resetting the
timer while on AC power, matching the documented behavior.

Fixes: https://github.com/systemd/systemd/issues/26498
man/systemd-sleep.conf.xml
src/sleep/sleep.c

index dee442b01e8f77dfccffa8ed4eacf658678c8adf..476e5ef21f4fb916f82eabdba60b832aa79254bf 100644 (file)
           <para>If the system has no battery, it would be hibernated after <varname>HibernateDelaySec=</varname>
           has passed. If not set, then defaults to <literal>2h</literal>.</para>
 
-          <para>If the system has battery and <varname>HibernateDelaySec=</varname> is not set, low-battery
-          alarms (ACPI _BTP) are tried first for detecting battery percentage and wake up the system for hibernation.
-          If not available, or <varname>HibernateDelaySec=</varname> is set, the system would regularly wake
-          up to check the time and detect the battery percentage/discharging rate. The rate is used to
-          schedule the next detection. If that is also not available, <varname>SuspendEstimationSec=</varname>
-          is used as last resort.</para>
+          <para>If the system has battery, low-battery alarms (ACPI _BTP) are tried first for detecting
+          battery percentage and wake up the system for hibernation. If <varname>HibernateDelaySec=</varname>
+          is also set, an additional timer is configured so that the system hibernates on whichever comes
+          first: low battery or the configured delay. If ACPI _BTP is not available, the system would
+          regularly wake up to check the time and detect the battery percentage/discharging rate. The rate
+          is used to schedule the next detection. If that is also not available,
+          <varname>SuspendEstimationSec=</varname> is used as last resort.</para>
 
           <xi:include href="version-info.xml" xpointer="v239"/>
         </listitem>
index 0d128053ba1afc5b65c7dac60c4604386b9dacb7..43aaede5b023aeeaf4c255b715c4c86889caa925 100644 (file)
@@ -507,37 +507,90 @@ static int custom_timer_suspend(const SleepConfig *sleep_config) {
 }
 
 static int execute_s2h(const SleepConfig *sleep_config) {
+        _cleanup_close_ int tfd = -EBADF;
+        usec_t hibernate_timestamp = 0;
         int r;
 
         assert(sleep_config);
 
-        /* Only check if we have automated battery alarms if HibernateDelaySec= is not set, as in that case
-         * we'll busy poll for the configured interval instead */
-        if (!timestamp_is_set(sleep_config->hibernate_delay_usec)) {
-                r = check_wakeup_type();
+        /* Always check if we have automated battery alarms, regardless of HibernateDelaySec= setting.
+         * This allows both low-battery hibernation AND timeout-based hibernation to work together. */
+        r = check_wakeup_type();
+        if (r < 0)
+                log_warning_errno(r, "Failed to check hardware wakeup type, ignoring: %m");
+        else {
+                r = battery_trip_point_alarm_exists();
                 if (r < 0)
-                        log_warning_errno(r, "Failed to check hardware wakeup type, ignoring: %m");
-                else {
-                        r = battery_trip_point_alarm_exists();
-                        if (r < 0)
-                                log_warning_errno(r, "Failed to check whether acpi_btp support is enabled or not, ignoring: %m");
+                        log_warning_errno(r, "Failed to check whether acpi_btp support is enabled or not, ignoring: %m");
+        }
+
+        if (r > 0) {
+                /* We have hardware battery alarm support (ACPI _BTP). If HibernateDelaySec= is also set,
+                 * set up an RTC alarm so we hibernate on whichever comes first: low battery or timeout. */
+                if (timestamp_is_set(sleep_config->hibernate_delay_usec)) {
+                        tfd = timerfd_create(CLOCK_BOOTTIME_ALARM, TFD_NONBLOCK|TFD_CLOEXEC);
+                        if (tfd < 0)
+                                return log_error_errno(errno, "Error creating timerfd: %m");
+
+                        hibernate_timestamp = usec_add(now(CLOCK_BOOTTIME), sleep_config->hibernate_delay_usec);
                 }
-        } else
-                r = 0;  /* Force fallback path */
 
-        if (r > 0) { /* If we have both wakeup alarms and battery trip point support, use them */
-                log_debug("Attempting to suspend...");
-                r = execute(sleep_config, SLEEP_SUSPEND, NULL);
-                if (r < 0)
-                        return r;
+                for (;;) {
+                        if (tfd >= 0) {
+                                struct itimerspec ts = {};
+                                usec_t time_left;
 
-                r = check_wakeup_type();
-                if (r < 0)
-                        return log_error_errno(r, "Failed to check hardware wakeup type: %m");
+                                /* Handle HibernateOnACPower=no: reset timer while on AC power */
+                                if (!sleep_config->hibernate_on_ac_power && on_ac_power() > 0) {
+                                        log_debug("On AC power with HibernateOnACPower=no, resetting hibernate timer");
+                                        hibernate_timestamp = usec_add(now(CLOCK_BOOTTIME), sleep_config->hibernate_delay_usec);
+                                }
 
-                if (r == 0)
-                        /* For APM Timer wakeup, system should hibernate else wakeup */
+                                time_left = usec_sub_unsigned(hibernate_timestamp, now(CLOCK_BOOTTIME));
+                                if (time_left <= 0)
+                                        break; /* Timer expired, hibernate */
+
+                                log_debug("Set timerfd wake alarm for %s",
+                                          FORMAT_TIMESPAN(time_left, USEC_PER_SEC));
+                                timespec_store(&ts.it_value, time_left);
+
+                                if (timerfd_settime(tfd, 0, &ts, NULL) < 0)
+                                        return log_error_errno(errno, "Error setting hibernate delay timer: %m");
+                        }
+
+                        log_debug("Attempting to suspend...");
+                        r = execute(sleep_config, SLEEP_SUSPEND, NULL);
+                        if (r < 0)
+                                return r;
+
+                        r = check_wakeup_type();
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to check hardware wakeup type: %m");
+                        if (r > 0) {
+                                /* APM Timer wakeup - this means the battery alarm triggered, hibernate */
+                                log_debug("Woken by APM Timer (battery low), proceeding to hibernate");
+                                break;
+                        }
+
+                        if (tfd >= 0) {
+                                /* Check if our HibernateDelaySec timer fired */
+                                r = fd_wait_for_event(tfd, POLLIN, 0);
+                                if (r < 0)
+                                        return log_error_errno(r, "Error polling timerfd: %m");
+                                if (FLAGS_SET(r, POLLIN)) {
+                                        /* Timer fired - but respect HibernateOnACPower setting */
+                                        if (!sleep_config->hibernate_on_ac_power && on_ac_power() > 0) {
+                                                log_debug("Timer fired but on AC power with HibernateOnACPower=no, continuing suspend");
+                                                continue;
+                                        }
+                                        log_debug("HibernateDelaySec timeout reached, proceeding to hibernate");
+                                        break;
+                                }
+                        }
+
+                        /* Manual wakeup - not battery alarm and not timer */
                         return 0;
+                }
         } else {
                 r = custom_timer_suspend(sleep_config);
                 if (r < 0)