]>
Commit | Line | Data |
---|---|---|
54d7fcc6 MY |
1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
2 | ||
3 | #include <unistd.h> | |
4 | ||
5 | #include "alloc-util.h" | |
6 | #include "conf-parser.h" | |
7 | #include "constants.h" | |
8 | #include "device-util.h" | |
9 | #include "devnum-util.h" | |
10 | #include "errno-util.h" | |
11 | #include "fd-util.h" | |
12 | #include "fileio.h" | |
13 | #include "hibernate-util.h" | |
14 | #include "log.h" | |
15 | #include "macro.h" | |
16 | #include "path-util.h" | |
17 | #include "sleep-config.h" | |
18 | #include "stat-util.h" | |
19 | #include "stdio-util.h" | |
20 | #include "string-table.h" | |
21 | #include "string-util.h" | |
22 | #include "strv.h" | |
23 | #include "time-util.h" | |
24 | ||
23577f44 MY |
25 | #define DEFAULT_SUSPEND_ESTIMATION_USEC (1 * USEC_PER_HOUR) |
26 | ||
087a25d2 MY |
27 | static const char* const sleep_operation_table[_SLEEP_OPERATION_MAX] = { |
28 | [SLEEP_SUSPEND] = "suspend", | |
29 | [SLEEP_HIBERNATE] = "hibernate", | |
30 | [SLEEP_HYBRID_SLEEP] = "hybrid-sleep", | |
31 | [SLEEP_SUSPEND_THEN_HIBERNATE] = "suspend-then-hibernate", | |
32 | }; | |
33 | ||
34 | DEFINE_STRING_TABLE_LOOKUP(sleep_operation, SleepOperation); | |
35 | ||
1f82c21d MY |
36 | static char* const* const sleep_default_state_table[_SLEEP_OPERATION_CONFIG_MAX] = { |
37 | [SLEEP_SUSPEND] = STRV_MAKE("mem", "standby", "freeze"), | |
38 | [SLEEP_HIBERNATE] = STRV_MAKE("disk"), | |
39 | [SLEEP_HYBRID_SLEEP] = STRV_MAKE("disk"), | |
40 | }; | |
41 | ||
42 | static char* const* const sleep_default_mode_table[_SLEEP_OPERATION_CONFIG_MAX] = { | |
43 | /* Not used by SLEEP_SUSPEND */ | |
44 | [SLEEP_HIBERNATE] = STRV_MAKE("platform", "shutdown"), | |
828ad304 | 45 | [SLEEP_HYBRID_SLEEP] = STRV_MAKE("suspend"), |
1f82c21d MY |
46 | }; |
47 | ||
087a25d2 MY |
48 | SleepConfig* sleep_config_free(SleepConfig *sc) { |
49 | if (!sc) | |
50 | return NULL; | |
51 | ||
52 | for (SleepOperation i = 0; i < _SLEEP_OPERATION_CONFIG_MAX; i++) { | |
087a25d2 | 53 | strv_free(sc->states[i]); |
1f82c21d | 54 | strv_free(sc->modes[i]); |
087a25d2 MY |
55 | } |
56 | ||
a2124b35 MY |
57 | strv_free(sc->mem_modes); |
58 | ||
087a25d2 MY |
59 | return mfree(sc); |
60 | } | |
61 | ||
77bd3938 MY |
62 | static int config_parse_sleep_mode( |
63 | const char *unit, | |
64 | const char *filename, | |
65 | unsigned line, | |
66 | const char *section, | |
67 | unsigned section_line, | |
68 | const char *lvalue, | |
69 | int ltype, | |
70 | const char *rvalue, | |
71 | void *data, | |
72 | void *userdata) { | |
73 | ||
77bd3938 | 74 | char ***sv = ASSERT_PTR(data); |
a6fe020a | 75 | _cleanup_strv_free_ char **modes = NULL; |
77bd3938 MY |
76 | int r; |
77 | ||
78 | assert(filename); | |
79 | assert(lvalue); | |
80 | assert(rvalue); | |
81 | ||
82 | if (isempty(rvalue)) { | |
83 | modes = strv_new(NULL); | |
84 | if (!modes) | |
85 | return log_oom(); | |
86 | } else { | |
87 | r = strv_split_full(&modes, rvalue, NULL, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE); | |
88 | if (r < 0) | |
89 | return log_oom(); | |
90 | } | |
91 | ||
a6fe020a | 92 | return strv_free_and_replace(*sv, modes); |
77bd3938 MY |
93 | } |
94 | ||
1f82c21d MY |
95 | static void sleep_config_validate_state_and_mode(SleepConfig *sc) { |
96 | assert(sc); | |
97 | ||
98 | /* So we should really not allow setting SuspendState= to 'disk', which means hibernation. We have | |
99 | * SLEEP_HIBERNATE for proper hibernation support, which includes checks for resume support (through | |
100 | * EFI variable or resume= kernel command line option). It's simply not sensible to call the suspend | |
101 | * operation but eventually do an unsafe hibernation. */ | |
102 | if (strv_contains(sc->states[SLEEP_SUSPEND], "disk")) { | |
103 | strv_remove(sc->states[SLEEP_SUSPEND], "disk"); | |
104 | log_warning("Sleep state 'disk' is not supported by operation %s, ignoring.", | |
105 | sleep_operation_to_string(SLEEP_SUSPEND)); | |
106 | } | |
107 | assert(!sc->modes[SLEEP_SUSPEND]); | |
108 | ||
109 | /* People should use hybrid-sleep instead of setting HibernateMode=suspend. Warn about it but don't | |
110 | * drop it in this case. */ | |
111 | if (strv_contains(sc->modes[SLEEP_HIBERNATE], "suspend")) | |
112 | log_warning("Sleep mode 'suspend' should not be used by operation %s. Please use %s instead.", | |
113 | sleep_operation_to_string(SLEEP_HIBERNATE), sleep_operation_to_string(SLEEP_HYBRID_SLEEP)); | |
1f82c21d MY |
114 | } |
115 | ||
087a25d2 MY |
116 | int parse_sleep_config(SleepConfig **ret) { |
117 | _cleanup_(sleep_config_freep) SleepConfig *sc = NULL; | |
118 | int allow_suspend = -1, allow_hibernate = -1, allow_s2h = -1, allow_hybrid_sleep = -1; | |
119 | ||
120 | assert(ret); | |
54d7fcc6 MY |
121 | |
122 | sc = new(SleepConfig, 1); | |
123 | if (!sc) | |
124 | return log_oom(); | |
125 | ||
126 | *sc = (SleepConfig) { | |
127 | .hibernate_delay_usec = USEC_INFINITY, | |
128 | }; | |
129 | ||
130 | const ConfigTableItem items[] = { | |
828ad304 MY |
131 | { "Sleep", "AllowSuspend", config_parse_tristate, 0, &allow_suspend }, |
132 | { "Sleep", "AllowHibernation", config_parse_tristate, 0, &allow_hibernate }, | |
133 | { "Sleep", "AllowSuspendThenHibernate", config_parse_tristate, 0, &allow_s2h }, | |
134 | { "Sleep", "AllowHybridSleep", config_parse_tristate, 0, &allow_hybrid_sleep }, | |
1f82c21d | 135 | |
828ad304 MY |
136 | { "Sleep", "SuspendState", config_parse_strv, 0, sc->states + SLEEP_SUSPEND }, |
137 | { "Sleep", "SuspendMode", config_parse_warn_compat, DISABLED_LEGACY, NULL }, | |
1f82c21d | 138 | |
828ad304 | 139 | { "Sleep", "HibernateState", config_parse_warn_compat, DISABLED_LEGACY, NULL }, |
77bd3938 | 140 | { "Sleep", "HibernateMode", config_parse_sleep_mode, 0, sc->modes + SLEEP_HIBERNATE }, |
1f82c21d | 141 | |
828ad304 MY |
142 | { "Sleep", "HybridSleepState", config_parse_warn_compat, DISABLED_LEGACY, NULL }, |
143 | { "Sleep", "HybridSleepMode", config_parse_warn_compat, DISABLED_LEGACY, NULL }, | |
1f82c21d | 144 | |
36b21fac | 145 | { "Sleep", "MemorySleepMode", config_parse_sleep_mode, 0, &sc->mem_modes }, |
a2124b35 | 146 | |
828ad304 MY |
147 | { "Sleep", "HibernateDelaySec", config_parse_sec, 0, &sc->hibernate_delay_usec }, |
148 | { "Sleep", "SuspendEstimationSec", config_parse_sec, 0, &sc->suspend_estimation_usec }, | |
54d7fcc6 MY |
149 | {} |
150 | }; | |
151 | ||
6378f257 ZJS |
152 | (void) config_parse_standard_file_with_dropins( |
153 | "systemd/sleep.conf", | |
154 | "Sleep\0", | |
155 | config_item_table_lookup, items, | |
156 | CONFIG_PARSE_WARN, | |
157 | /* userdata= */ NULL); | |
54d7fcc6 MY |
158 | |
159 | /* use default values unless set */ | |
160 | sc->allow[SLEEP_SUSPEND] = allow_suspend != 0; | |
161 | sc->allow[SLEEP_HIBERNATE] = allow_hibernate != 0; | |
162 | sc->allow[SLEEP_HYBRID_SLEEP] = allow_hybrid_sleep >= 0 ? allow_hybrid_sleep | |
163 | : (allow_suspend != 0 && allow_hibernate != 0); | |
164 | sc->allow[SLEEP_SUSPEND_THEN_HIBERNATE] = allow_s2h >= 0 ? allow_s2h | |
165 | : (allow_suspend != 0 && allow_hibernate != 0); | |
166 | ||
1f82c21d MY |
167 | for (SleepOperation i = 0; i < _SLEEP_OPERATION_CONFIG_MAX; i++) { |
168 | if (!sc->states[i] && sleep_default_state_table[i]) { | |
169 | sc->states[i] = strv_copy(sleep_default_state_table[i]); | |
170 | if (!sc->states[i]) | |
171 | return log_oom(); | |
172 | } | |
173 | ||
174 | if (!sc->modes[i] && sleep_default_mode_table[i]) { | |
175 | sc->modes[i] = strv_copy(sleep_default_mode_table[i]); | |
176 | if (!sc->modes[i]) | |
177 | return log_oom(); | |
178 | } | |
179 | } | |
180 | ||
54d7fcc6 MY |
181 | if (sc->suspend_estimation_usec == 0) |
182 | sc->suspend_estimation_usec = DEFAULT_SUSPEND_ESTIMATION_USEC; | |
183 | ||
1f82c21d | 184 | sleep_config_validate_state_and_mode(sc); |
54d7fcc6 | 185 | |
087a25d2 | 186 | *ret = TAKE_PTR(sc); |
54d7fcc6 MY |
187 | return 0; |
188 | } | |
189 | ||
b0e3b85a | 190 | int sleep_state_supported(char * const *states) { |
23577f44 MY |
191 | _cleanup_free_ char *supported_sysfs = NULL; |
192 | const char *found; | |
54d7fcc6 MY |
193 | int r; |
194 | ||
23577f44 MY |
195 | if (strv_isempty(states)) |
196 | return log_debug_errno(SYNTHETIC_ERRNO(ENOMSG), "No sleep state configured."); | |
54d7fcc6 | 197 | |
23577f44 MY |
198 | if (access("/sys/power/state", W_OK) < 0) |
199 | return log_debug_errno(errno, "/sys/power/state is not writable: %m"); | |
54d7fcc6 | 200 | |
23577f44 MY |
201 | r = read_one_line_file("/sys/power/state", &supported_sysfs); |
202 | if (r < 0) | |
203 | return log_debug_errno(r, "Failed to read /sys/power/state: %m"); | |
54d7fcc6 | 204 | |
23577f44 | 205 | r = string_contains_word_strv(supported_sysfs, NULL, states, &found); |
54d7fcc6 MY |
206 | if (r < 0) |
207 | return log_debug_errno(r, "Failed to parse /sys/power/state: %m"); | |
23577f44 MY |
208 | if (r > 0) { |
209 | log_debug("Sleep state '%s' is supported by kernel.", found); | |
210 | return true; | |
54d7fcc6 | 211 | } |
23577f44 MY |
212 | |
213 | if (DEBUG_LOGGING) { | |
214 | _cleanup_free_ char *joined = strv_join(states, " "); | |
215 | log_debug("None of the configured sleep states are supported by kernel: %s", strnull(joined)); | |
216 | } | |
217 | return false; | |
54d7fcc6 MY |
218 | } |
219 | ||
b0e3b85a | 220 | int sleep_mode_supported(const char *path, char * const *modes) { |
23577f44 | 221 | _cleanup_free_ char *supported_sysfs = NULL; |
54d7fcc6 MY |
222 | int r; |
223 | ||
b0e3b85a MY |
224 | assert(path); |
225 | ||
23577f44 MY |
226 | /* Unlike state, kernel has its own default choice if not configured */ |
227 | if (strv_isempty(modes)) { | |
b0e3b85a | 228 | log_debug("No sleep mode configured, using kernel default for %s.", path); |
54d7fcc6 | 229 | return true; |
54d7fcc6 MY |
230 | } |
231 | ||
b0e3b85a MY |
232 | if (access(path, W_OK) < 0) |
233 | return log_debug_errno(errno, "%s is not writable: %m", path); | |
54d7fcc6 | 234 | |
b0e3b85a | 235 | r = read_one_line_file(path, &supported_sysfs); |
23577f44 | 236 | if (r < 0) |
b0e3b85a | 237 | return log_debug_errno(r, "Failed to read %s: %m", path); |
23577f44 MY |
238 | |
239 | for (const char *p = supported_sysfs;;) { | |
54d7fcc6 | 240 | _cleanup_free_ char *word = NULL; |
23577f44 MY |
241 | char *mode; |
242 | size_t l; | |
54d7fcc6 MY |
243 | |
244 | r = extract_first_word(&p, &word, NULL, 0); | |
245 | if (r < 0) | |
b0e3b85a | 246 | return log_debug_errno(r, "Failed to parse %s: %m", path); |
54d7fcc6 MY |
247 | if (r == 0) |
248 | break; | |
249 | ||
23577f44 MY |
250 | mode = word; |
251 | l = strlen(word); | |
252 | ||
253 | if (mode[0] == '[' && mode[l - 1] == ']') { | |
254 | mode[l - 1] = '\0'; | |
255 | mode++; | |
54d7fcc6 MY |
256 | } |
257 | ||
23577f44 | 258 | if (strv_contains(modes, mode)) { |
b0e3b85a | 259 | log_debug("Sleep mode '%s' is supported by kernel (%s).", mode, path); |
54d7fcc6 MY |
260 | return true; |
261 | } | |
262 | } | |
263 | ||
264 | if (DEBUG_LOGGING) { | |
23577f44 | 265 | _cleanup_free_ char *joined = strv_join(modes, " "); |
b0e3b85a MY |
266 | log_debug("None of the configured modes are supported by kernel (%s): %s", |
267 | path, strnull(joined)); | |
54d7fcc6 MY |
268 | } |
269 | return false; | |
270 | } | |
271 | ||
a0f6d74e MY |
272 | static int sleep_supported_internal( |
273 | const SleepConfig *sleep_config, | |
274 | SleepOperation operation, | |
275 | bool check_allowed, | |
276 | SleepSupport *ret_support); | |
54d7fcc6 | 277 | |
a0f6d74e | 278 | static int s2h_supported(const SleepConfig *sleep_config, SleepSupport *ret_support) { |
54d7fcc6 MY |
279 | |
280 | static const SleepOperation operations[] = { | |
281 | SLEEP_SUSPEND, | |
282 | SLEEP_HIBERNATE, | |
283 | }; | |
284 | ||
a0f6d74e | 285 | SleepSupport support; |
54d7fcc6 MY |
286 | int r; |
287 | ||
a0f6d74e MY |
288 | assert(sleep_config); |
289 | assert(ret_support); | |
290 | ||
54d7fcc6 | 291 | if (!clock_supported(CLOCK_BOOTTIME_ALARM)) { |
a0f6d74e MY |
292 | log_debug("CLOCK_BOOTTIME_ALARM is not supported, can't perform %s.", sleep_operation_to_string(SLEEP_SUSPEND_THEN_HIBERNATE)); |
293 | *ret_support = SLEEP_ALARM_NOT_SUPPORTED; | |
54d7fcc6 MY |
294 | return false; |
295 | } | |
296 | ||
85471164 | 297 | FOREACH_ELEMENT(i, operations) { |
a0f6d74e MY |
298 | r = sleep_supported_internal(sleep_config, *i, /* check_allowed = */ false, &support); |
299 | if (r < 0) | |
300 | return r; | |
301 | if (r == 0) { | |
302 | log_debug("Sleep operation %s is not supported, can't perform %s.", | |
303 | sleep_operation_to_string(*i), sleep_operation_to_string(SLEEP_SUSPEND_THEN_HIBERNATE)); | |
304 | *ret_support = support; | |
54d7fcc6 MY |
305 | return false; |
306 | } | |
54d7fcc6 MY |
307 | } |
308 | ||
a0f6d74e MY |
309 | assert(support == SLEEP_SUPPORTED); |
310 | *ret_support = support; | |
311 | ||
54d7fcc6 MY |
312 | return true; |
313 | } | |
314 | ||
a0f6d74e | 315 | static int sleep_supported_internal( |
54d7fcc6 MY |
316 | const SleepConfig *sleep_config, |
317 | SleepOperation operation, | |
a0f6d74e MY |
318 | bool check_allowed, |
319 | SleepSupport *ret_support) { | |
320 | ||
321 | int r; | |
54d7fcc6 | 322 | |
a0f6d74e | 323 | assert(sleep_config); |
54d7fcc6 MY |
324 | assert(operation >= 0); |
325 | assert(operation < _SLEEP_OPERATION_MAX); | |
a0f6d74e | 326 | assert(ret_support); |
54d7fcc6 MY |
327 | |
328 | if (check_allowed && !sleep_config->allow[operation]) { | |
a0f6d74e MY |
329 | log_debug("Sleep operation %s is disabled by configuration.", sleep_operation_to_string(operation)); |
330 | *ret_support = SLEEP_DISABLED; | |
54d7fcc6 MY |
331 | return false; |
332 | } | |
333 | ||
334 | if (operation == SLEEP_SUSPEND_THEN_HIBERNATE) | |
a0f6d74e | 335 | return s2h_supported(sleep_config, ret_support); |
54d7fcc6 | 336 | |
a0f6d74e MY |
337 | assert(operation < _SLEEP_OPERATION_CONFIG_MAX); |
338 | ||
339 | r = sleep_state_supported(sleep_config->states[operation]); | |
340 | if (r == -ENOMSG) { | |
341 | *ret_support = SLEEP_NOT_CONFIGURED; | |
54d7fcc6 | 342 | return false; |
a0f6d74e MY |
343 | } |
344 | if (r < 0) | |
345 | return r; | |
346 | if (r == 0) { | |
347 | *ret_support = SLEEP_STATE_OR_MODE_NOT_SUPPORTED; | |
348 | return false; | |
349 | } | |
54d7fcc6 | 350 | |
a2124b35 MY |
351 | if (SLEEP_NEEDS_MEM_SLEEP(sleep_config, operation)) { |
352 | r = sleep_mode_supported("/sys/power/mem_sleep", sleep_config->mem_modes); | |
353 | if (r < 0) | |
354 | return r; | |
355 | if (r == 0) { | |
356 | *ret_support = SLEEP_STATE_OR_MODE_NOT_SUPPORTED; | |
357 | return false; | |
358 | } | |
359 | } | |
360 | ||
792dd6f4 | 361 | if (SLEEP_OPERATION_IS_HIBERNATION(operation)) { |
b0e3b85a | 362 | r = sleep_mode_supported("/sys/power/disk", sleep_config->modes[operation]); |
fefddffa MY |
363 | if (r < 0) |
364 | return r; | |
365 | if (r == 0) { | |
366 | *ret_support = SLEEP_STATE_OR_MODE_NOT_SUPPORTED; | |
367 | return false; | |
368 | } | |
369 | ||
805deec0 MY |
370 | r = hibernation_is_safe(); |
371 | if (r == -ENOTRECOVERABLE) { | |
372 | *ret_support = SLEEP_RESUME_NOT_SUPPORTED; | |
373 | return false; | |
374 | } | |
375 | if (r == -ENOSPC) { | |
376 | *ret_support = SLEEP_NOT_ENOUGH_SWAP_SPACE; | |
377 | return false; | |
378 | } | |
379 | if (r < 0) | |
380 | return r; | |
fefddffa MY |
381 | } else |
382 | assert(!sleep_config->modes[operation]); | |
54d7fcc6 | 383 | |
a0f6d74e | 384 | *ret_support = SLEEP_SUPPORTED; |
54d7fcc6 MY |
385 | return true; |
386 | } | |
387 | ||
a0f6d74e | 388 | int sleep_supported_full(SleepOperation operation, SleepSupport *ret_support) { |
087a25d2 | 389 | _cleanup_(sleep_config_freep) SleepConfig *sleep_config = NULL; |
a0f6d74e | 390 | SleepSupport support; |
54d7fcc6 MY |
391 | int r; |
392 | ||
a0f6d74e MY |
393 | assert(operation >= 0); |
394 | assert(operation < _SLEEP_OPERATION_MAX); | |
395 | ||
54d7fcc6 MY |
396 | r = parse_sleep_config(&sleep_config); |
397 | if (r < 0) | |
398 | return r; | |
399 | ||
a0f6d74e MY |
400 | r = sleep_supported_internal(sleep_config, operation, /* check_allowed = */ true, &support); |
401 | if (r < 0) | |
402 | return r; | |
403 | ||
404 | assert((r > 0) == (support == SLEEP_SUPPORTED)); | |
405 | ||
406 | if (ret_support) | |
407 | *ret_support = support; | |
408 | ||
409 | return r; | |
54d7fcc6 | 410 | } |