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