diff options
Diffstat (limited to 'src/shared/sleep-config.c')
-rw-r--r-- | src/shared/sleep-config.c | 390 |
1 files changed, 390 insertions, 0 deletions
diff --git a/src/shared/sleep-config.c b/src/shared/sleep-config.c new file mode 100644 index 0000000..7282111 --- /dev/null +++ b/src/shared/sleep-config.c @@ -0,0 +1,390 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <unistd.h> + +#include "alloc-util.h" +#include "conf-parser.h" +#include "constants.h" +#include "device-util.h" +#include "devnum-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "hibernate-util.h" +#include "log.h" +#include "macro.h" +#include "path-util.h" +#include "sleep-config.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "time-util.h" + +#define DEFAULT_SUSPEND_ESTIMATION_USEC (1 * USEC_PER_HOUR) + +static const char* const sleep_operation_table[_SLEEP_OPERATION_MAX] = { + [SLEEP_SUSPEND] = "suspend", + [SLEEP_HIBERNATE] = "hibernate", + [SLEEP_HYBRID_SLEEP] = "hybrid-sleep", + [SLEEP_SUSPEND_THEN_HIBERNATE] = "suspend-then-hibernate", +}; + +DEFINE_STRING_TABLE_LOOKUP(sleep_operation, SleepOperation); + +static char* const* const sleep_default_state_table[_SLEEP_OPERATION_CONFIG_MAX] = { + [SLEEP_SUSPEND] = STRV_MAKE("mem", "standby", "freeze"), + [SLEEP_HIBERNATE] = STRV_MAKE("disk"), + [SLEEP_HYBRID_SLEEP] = STRV_MAKE("disk"), +}; + +static char* const* const sleep_default_mode_table[_SLEEP_OPERATION_CONFIG_MAX] = { + /* Not used by SLEEP_SUSPEND */ + [SLEEP_HIBERNATE] = STRV_MAKE("platform", "shutdown"), + [SLEEP_HYBRID_SLEEP] = STRV_MAKE("suspend"), +}; + +SleepConfig* sleep_config_free(SleepConfig *sc) { + if (!sc) + return NULL; + + for (SleepOperation i = 0; i < _SLEEP_OPERATION_CONFIG_MAX; i++) { + strv_free(sc->states[i]); + strv_free(sc->modes[i]); + } + + return mfree(sc); +} + +static int config_parse_sleep_mode( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_strv_free_ char **modes = NULL; + char ***sv = ASSERT_PTR(data); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + modes = strv_new(NULL); + if (!modes) + return log_oom(); + } else { + r = strv_split_full(&modes, rvalue, NULL, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE); + if (r < 0) + return log_oom(); + } + + return free_and_replace(*sv, modes); +} + +static void sleep_config_validate_state_and_mode(SleepConfig *sc) { + assert(sc); + + /* So we should really not allow setting SuspendState= to 'disk', which means hibernation. We have + * SLEEP_HIBERNATE for proper hibernation support, which includes checks for resume support (through + * EFI variable or resume= kernel command line option). It's simply not sensible to call the suspend + * operation but eventually do an unsafe hibernation. */ + if (strv_contains(sc->states[SLEEP_SUSPEND], "disk")) { + strv_remove(sc->states[SLEEP_SUSPEND], "disk"); + log_warning("Sleep state 'disk' is not supported by operation %s, ignoring.", + sleep_operation_to_string(SLEEP_SUSPEND)); + } + assert(!sc->modes[SLEEP_SUSPEND]); + + /* People should use hybrid-sleep instead of setting HibernateMode=suspend. Warn about it but don't + * drop it in this case. */ + if (strv_contains(sc->modes[SLEEP_HIBERNATE], "suspend")) + log_warning("Sleep mode 'suspend' should not be used by operation %s. Please use %s instead.", + sleep_operation_to_string(SLEEP_HIBERNATE), sleep_operation_to_string(SLEEP_HYBRID_SLEEP)); +} + +int parse_sleep_config(SleepConfig **ret) { + _cleanup_(sleep_config_freep) SleepConfig *sc = NULL; + int allow_suspend = -1, allow_hibernate = -1, allow_s2h = -1, allow_hybrid_sleep = -1; + + assert(ret); + + sc = new(SleepConfig, 1); + if (!sc) + return log_oom(); + + *sc = (SleepConfig) { + .hibernate_delay_usec = USEC_INFINITY, + }; + + const ConfigTableItem items[] = { + { "Sleep", "AllowSuspend", config_parse_tristate, 0, &allow_suspend }, + { "Sleep", "AllowHibernation", config_parse_tristate, 0, &allow_hibernate }, + { "Sleep", "AllowSuspendThenHibernate", config_parse_tristate, 0, &allow_s2h }, + { "Sleep", "AllowHybridSleep", config_parse_tristate, 0, &allow_hybrid_sleep }, + + { "Sleep", "SuspendState", config_parse_strv, 0, sc->states + SLEEP_SUSPEND }, + { "Sleep", "SuspendMode", config_parse_warn_compat, DISABLED_LEGACY, NULL }, + + { "Sleep", "HibernateState", config_parse_warn_compat, DISABLED_LEGACY, NULL }, + { "Sleep", "HibernateMode", config_parse_sleep_mode, 0, sc->modes + SLEEP_HIBERNATE }, + + { "Sleep", "HybridSleepState", config_parse_warn_compat, DISABLED_LEGACY, NULL }, + { "Sleep", "HybridSleepMode", config_parse_warn_compat, DISABLED_LEGACY, NULL }, + + { "Sleep", "HibernateDelaySec", config_parse_sec, 0, &sc->hibernate_delay_usec }, + { "Sleep", "SuspendEstimationSec", config_parse_sec, 0, &sc->suspend_estimation_usec }, + {} + }; + + (void) config_parse_config_file("sleep.conf", "Sleep\0", + config_item_table_lookup, items, + CONFIG_PARSE_WARN, NULL); + + /* use default values unless set */ + sc->allow[SLEEP_SUSPEND] = allow_suspend != 0; + sc->allow[SLEEP_HIBERNATE] = allow_hibernate != 0; + sc->allow[SLEEP_HYBRID_SLEEP] = allow_hybrid_sleep >= 0 ? allow_hybrid_sleep + : (allow_suspend != 0 && allow_hibernate != 0); + sc->allow[SLEEP_SUSPEND_THEN_HIBERNATE] = allow_s2h >= 0 ? allow_s2h + : (allow_suspend != 0 && allow_hibernate != 0); + + for (SleepOperation i = 0; i < _SLEEP_OPERATION_CONFIG_MAX; i++) { + if (!sc->states[i] && sleep_default_state_table[i]) { + sc->states[i] = strv_copy(sleep_default_state_table[i]); + if (!sc->states[i]) + return log_oom(); + } + + if (!sc->modes[i] && sleep_default_mode_table[i]) { + sc->modes[i] = strv_copy(sleep_default_mode_table[i]); + if (!sc->modes[i]) + return log_oom(); + } + } + + if (sc->suspend_estimation_usec == 0) + sc->suspend_estimation_usec = DEFAULT_SUSPEND_ESTIMATION_USEC; + + sleep_config_validate_state_and_mode(sc); + + *ret = TAKE_PTR(sc); + return 0; +} + +int sleep_state_supported(char **states) { + _cleanup_free_ char *supported_sysfs = NULL; + const char *found; + int r; + + if (strv_isempty(states)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOMSG), "No sleep state configured."); + + if (access("/sys/power/state", W_OK) < 0) + return log_debug_errno(errno, "/sys/power/state is not writable: %m"); + + r = read_one_line_file("/sys/power/state", &supported_sysfs); + if (r < 0) + return log_debug_errno(r, "Failed to read /sys/power/state: %m"); + + r = string_contains_word_strv(supported_sysfs, NULL, states, &found); + if (r < 0) + return log_debug_errno(r, "Failed to parse /sys/power/state: %m"); + if (r > 0) { + log_debug("Sleep state '%s' is supported by kernel.", found); + return true; + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *joined = strv_join(states, " "); + log_debug("None of the configured sleep states are supported by kernel: %s", strnull(joined)); + } + return false; +} + +int sleep_mode_supported(char **modes) { + _cleanup_free_ char *supported_sysfs = NULL; + int r; + + /* Unlike state, kernel has its own default choice if not configured */ + if (strv_isempty(modes)) { + log_debug("No sleep mode configured, using kernel default."); + return true; + } + + if (access("/sys/power/disk", W_OK) < 0) + return log_debug_errno(errno, "/sys/power/disk is not writable: %m"); + + r = read_one_line_file("/sys/power/disk", &supported_sysfs); + if (r < 0) + return log_debug_errno(r, "Failed to read /sys/power/disk: %m"); + + for (const char *p = supported_sysfs;;) { + _cleanup_free_ char *word = NULL; + char *mode; + size_t l; + + r = extract_first_word(&p, &word, NULL, 0); + if (r < 0) + return log_debug_errno(r, "Failed to parse /sys/power/disk: %m"); + if (r == 0) + break; + + mode = word; + l = strlen(word); + + if (mode[0] == '[' && mode[l - 1] == ']') { + mode[l - 1] = '\0'; + mode++; + } + + if (strv_contains(modes, mode)) { + log_debug("Disk sleep mode '%s' is supported by kernel.", mode); + return true; + } + } + + if (DEBUG_LOGGING) { + _cleanup_free_ char *joined = strv_join(modes, " "); + log_debug("None of the configured hibernation power modes are supported by kernel: %s", strnull(joined)); + } + return false; +} + +static int sleep_supported_internal( + const SleepConfig *sleep_config, + SleepOperation operation, + bool check_allowed, + SleepSupport *ret_support); + +static int s2h_supported(const SleepConfig *sleep_config, SleepSupport *ret_support) { + + static const SleepOperation operations[] = { + SLEEP_SUSPEND, + SLEEP_HIBERNATE, + }; + + SleepSupport support; + int r; + + assert(sleep_config); + assert(ret_support); + + if (!clock_supported(CLOCK_BOOTTIME_ALARM)) { + log_debug("CLOCK_BOOTTIME_ALARM is not supported, can't perform %s.", sleep_operation_to_string(SLEEP_SUSPEND_THEN_HIBERNATE)); + *ret_support = SLEEP_ALARM_NOT_SUPPORTED; + return false; + } + + FOREACH_ARRAY(i, operations, ELEMENTSOF(operations)) { + r = sleep_supported_internal(sleep_config, *i, /* check_allowed = */ false, &support); + if (r < 0) + return r; + if (r == 0) { + log_debug("Sleep operation %s is not supported, can't perform %s.", + sleep_operation_to_string(*i), sleep_operation_to_string(SLEEP_SUSPEND_THEN_HIBERNATE)); + *ret_support = support; + return false; + } + } + + assert(support == SLEEP_SUPPORTED); + *ret_support = support; + + return true; +} + +static int sleep_supported_internal( + const SleepConfig *sleep_config, + SleepOperation operation, + bool check_allowed, + SleepSupport *ret_support) { + + int r; + + assert(sleep_config); + assert(operation >= 0); + assert(operation < _SLEEP_OPERATION_MAX); + assert(ret_support); + + if (check_allowed && !sleep_config->allow[operation]) { + log_debug("Sleep operation %s is disabled by configuration.", sleep_operation_to_string(operation)); + *ret_support = SLEEP_DISABLED; + return false; + } + + if (operation == SLEEP_SUSPEND_THEN_HIBERNATE) + return s2h_supported(sleep_config, ret_support); + + assert(operation < _SLEEP_OPERATION_CONFIG_MAX); + + r = sleep_state_supported(sleep_config->states[operation]); + if (r == -ENOMSG) { + *ret_support = SLEEP_NOT_CONFIGURED; + return false; + } + if (r < 0) + return r; + if (r == 0) { + *ret_support = SLEEP_STATE_OR_MODE_NOT_SUPPORTED; + return false; + } + + if (sleep_operation_is_hibernation(operation)) { + r = sleep_mode_supported(sleep_config->modes[operation]); + if (r < 0) + return r; + if (r == 0) { + *ret_support = SLEEP_STATE_OR_MODE_NOT_SUPPORTED; + return false; + } + + r = hibernation_is_safe(); + if (r == -ENOTRECOVERABLE) { + *ret_support = SLEEP_RESUME_NOT_SUPPORTED; + return false; + } + if (r == -ENOSPC) { + *ret_support = SLEEP_NOT_ENOUGH_SWAP_SPACE; + return false; + } + if (r < 0) + return r; + } else + assert(!sleep_config->modes[operation]); + + *ret_support = SLEEP_SUPPORTED; + return true; +} + +int sleep_supported_full(SleepOperation operation, SleepSupport *ret_support) { + _cleanup_(sleep_config_freep) SleepConfig *sleep_config = NULL; + SleepSupport support; + int r; + + assert(operation >= 0); + assert(operation < _SLEEP_OPERATION_MAX); + + r = parse_sleep_config(&sleep_config); + if (r < 0) + return r; + + r = sleep_supported_internal(sleep_config, operation, /* check_allowed = */ true, &support); + if (r < 0) + return r; + + assert((r > 0) == (support == SLEEP_SUPPORTED)); + + if (ret_support) + *ret_support = support; + + return r; +} |