]>
Commit | Line | Data |
---|---|---|
53e1b683 | 1 | /* SPDX-License-Identifier: LGPL-2.1+ */ |
3731acf1 | 2 | |
9aadd281 | 3 | #include "sd-device.h" |
b4bbcaa9 | 4 | |
b5efdb8a | 5 | #include "alloc-util.h" |
8437c059 | 6 | #include "device-util.h" |
4f5dd394 | 7 | #include "escape.h" |
3731acf1 | 8 | #include "fileio.h" |
1ddae15a | 9 | #include "main-func.h" |
4f5dd394 | 10 | #include "mkdir.h" |
4e731273 LP |
11 | #include "parse-util.h" |
12 | #include "proc-cmdline.h" | |
07630cea | 13 | #include "string-util.h" |
9aadd281 | 14 | #include "strv.h" |
4f5dd394 | 15 | #include "util.h" |
3731acf1 | 16 | |
9aadd281 YW |
17 | static int find_pci_or_platform_parent(sd_device *device, sd_device **ret) { |
18 | const char *subsystem, *sysname, *value; | |
19 | sd_device *parent; | |
20 | int r; | |
0f4ba83c LP |
21 | |
22 | assert(device); | |
9aadd281 | 23 | assert(ret); |
0f4ba83c | 24 | |
9aadd281 YW |
25 | r = sd_device_get_parent(device, &parent); |
26 | if (r < 0) | |
27 | return r; | |
0f4ba83c | 28 | |
9aadd281 YW |
29 | r = sd_device_get_subsystem(parent, &subsystem); |
30 | if (r < 0) | |
31 | return r; | |
0f4ba83c | 32 | |
9aadd281 YW |
33 | r = sd_device_get_sysname(parent, &sysname); |
34 | if (r < 0) | |
35 | return r; | |
0f4ba83c LP |
36 | |
37 | if (streq(subsystem, "drm")) { | |
38 | const char *c; | |
39 | ||
40 | c = startswith(sysname, "card"); | |
41 | if (!c) | |
9aadd281 | 42 | return -ENODATA; |
0f4ba83c | 43 | |
938d2699 | 44 | c += strspn(c, DIGITS); |
0f4ba83c LP |
45 | if (*c == '-') { |
46 | /* A connector DRM device, let's ignore all but LVDS and eDP! */ | |
49fe5c09 | 47 | if (!STARTSWITH_SET(c, "-LVDS-", "-Embedded DisplayPort-")) |
9aadd281 | 48 | return -EOPNOTSUPP; |
0f4ba83c LP |
49 | } |
50 | ||
9aadd281 YW |
51 | } else if (streq(subsystem, "pci") && |
52 | sd_device_get_sysattr_value(parent, "class", &value) >= 0) { | |
53 | unsigned long class = 0; | |
0f4ba83c | 54 | |
9aadd281 YW |
55 | r = safe_atolu(value, &class); |
56 | if (r < 0) | |
57 | return log_warning_errno(r, "Cannot parse PCI class '%s' of device %s:%s: %m", | |
58 | value, subsystem, sysname); | |
0f4ba83c | 59 | |
9aadd281 YW |
60 | /* Graphics card */ |
61 | if (class == 0x30000) { | |
e8596ca5 | 62 | *ret = parent; |
9aadd281 | 63 | return 0; |
0f4ba83c LP |
64 | } |
65 | ||
9aadd281 | 66 | } else if (streq(subsystem, "platform")) { |
e8596ca5 | 67 | *ret = parent; |
9aadd281 YW |
68 | return 0; |
69 | } | |
0f4ba83c | 70 | |
9aadd281 | 71 | return find_pci_or_platform_parent(parent, ret); |
0f4ba83c LP |
72 | } |
73 | ||
9aadd281 YW |
74 | static int same_device(sd_device *a, sd_device *b) { |
75 | const char *a_val, *b_val; | |
76 | int r; | |
77 | ||
0f4ba83c LP |
78 | assert(a); |
79 | assert(b); | |
80 | ||
9aadd281 YW |
81 | r = sd_device_get_subsystem(a, &a_val); |
82 | if (r < 0) | |
83 | return r; | |
84 | ||
85 | r = sd_device_get_subsystem(b, &b_val); | |
86 | if (r < 0) | |
87 | return r; | |
0f4ba83c | 88 | |
403660c5 | 89 | if (!streq(a_val, b_val)) |
0f4ba83c LP |
90 | return false; |
91 | ||
9aadd281 YW |
92 | r = sd_device_get_sysname(a, &a_val); |
93 | if (r < 0) | |
94 | return r; | |
95 | ||
96 | r = sd_device_get_sysname(b, &b_val); | |
97 | if (r < 0) | |
98 | return r; | |
99 | ||
403660c5 | 100 | return streq(a_val, b_val); |
0f4ba83c LP |
101 | } |
102 | ||
9aadd281 YW |
103 | static int validate_device(sd_device *device) { |
104 | _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *enumerate = NULL; | |
0f4ba83c | 105 | const char *v, *subsystem; |
e8596ca5 | 106 | sd_device *parent, *other; |
0f4ba83c LP |
107 | int r; |
108 | ||
0f4ba83c LP |
109 | assert(device); |
110 | ||
111 | /* Verify whether we should actually care for a specific | |
112 | * backlight device. For backlight devices there might be | |
113 | * multiple ways to access the same control: "firmware" | |
114 | * (i.e. ACPI), "platform" (i.e. via the machine's EC) and | |
115 | * "raw" (via the graphics card). In general we should prefer | |
116 | * "firmware" (i.e. ACPI) or "platform" access over "raw" | |
117 | * access, in order not to confuse the BIOS/EC, and | |
118 | * compatibility with possible low-level hotkey handling of | |
119 | * screen brightness. The kernel will already make sure to | |
120 | * expose only one of "firmware" and "platform" for the same | |
121 | * device to userspace. However, we still need to make sure | |
122 | * that we use "raw" only if no "firmware" or "platform" | |
123 | * device for the same device exists. */ | |
124 | ||
9aadd281 YW |
125 | r = sd_device_get_subsystem(device, &subsystem); |
126 | if (r < 0) | |
127 | return r; | |
128 | if (!streq(subsystem, "backlight")) | |
0f4ba83c LP |
129 | return true; |
130 | ||
9aadd281 YW |
131 | r = sd_device_get_sysattr_value(device, "type", &v); |
132 | if (r < 0) | |
133 | return r; | |
134 | if (!streq(v, "raw")) | |
0f4ba83c LP |
135 | return true; |
136 | ||
9aadd281 YW |
137 | r = find_pci_or_platform_parent(device, &parent); |
138 | if (r < 0) | |
139 | return r; | |
0f4ba83c | 140 | |
9aadd281 YW |
141 | r = sd_device_get_subsystem(parent, &subsystem); |
142 | if (r < 0) | |
143 | return r; | |
0f4ba83c | 144 | |
9aadd281 YW |
145 | r = sd_device_enumerator_new(&enumerate); |
146 | if (r < 0) | |
147 | return r; | |
0f4ba83c | 148 | |
9aadd281 | 149 | r = sd_device_enumerator_allow_uninitialized(enumerate); |
0f4ba83c | 150 | if (r < 0) |
9aadd281 | 151 | return r; |
0f4ba83c | 152 | |
9aadd281 | 153 | r = sd_device_enumerator_add_match_subsystem(enumerate, "backlight", true); |
0f4ba83c | 154 | if (r < 0) |
9aadd281 | 155 | return r; |
0f4ba83c | 156 | |
8437c059 | 157 | FOREACH_DEVICE(enumerate, other) { |
9aadd281 | 158 | const char *other_subsystem; |
e8596ca5 | 159 | sd_device *other_parent; |
0f4ba83c | 160 | |
9aadd281 | 161 | if (same_device(device, other) > 0) |
0f4ba83c LP |
162 | continue; |
163 | ||
9aadd281 YW |
164 | if (sd_device_get_sysattr_value(other, "type", &v) < 0 || |
165 | !STR_IN_SET(v, "platform", "firmware")) | |
0f4ba83c LP |
166 | continue; |
167 | ||
168 | /* OK, so there's another backlight device, and it's a | |
169 | * platform or firmware device, so, let's see if we | |
9aadd281 YW |
170 | * can verify it belongs to the same device as ours. */ |
171 | if (find_pci_or_platform_parent(other, &other_parent) < 0) | |
0f4ba83c LP |
172 | continue; |
173 | ||
174 | if (same_device(parent, other_parent)) { | |
9aadd281 YW |
175 | const char *device_sysname = NULL, *other_sysname = NULL; |
176 | ||
177 | /* Both have the same PCI parent, that means we are out. */ | |
178 | ||
179 | (void) sd_device_get_sysname(device, &device_sysname); | |
180 | (void) sd_device_get_sysname(other, &other_sysname); | |
181 | ||
938d2699 | 182 | log_debug("Skipping backlight device %s, since device %s is on same PCI device and takes precedence.", |
9aadd281 | 183 | device_sysname, other_sysname); |
0f4ba83c LP |
184 | return false; |
185 | } | |
186 | ||
9aadd281 YW |
187 | if (sd_device_get_subsystem(other_parent, &other_subsystem) < 0) |
188 | continue; | |
189 | ||
190 | if (streq(other_subsystem, "platform") && streq(subsystem, "pci")) { | |
191 | const char *device_sysname = NULL, *other_sysname = NULL; | |
192 | ||
193 | /* The other is connected to the platform bus and we are a PCI device, that also means we are out. */ | |
194 | ||
195 | (void) sd_device_get_sysname(device, &device_sysname); | |
196 | (void) sd_device_get_sysname(other, &other_sysname); | |
197 | ||
938d2699 | 198 | log_debug("Skipping backlight device %s, since device %s is a platform device and takes precedence.", |
9aadd281 | 199 | device_sysname, other_sysname); |
0f4ba83c LP |
200 | return false; |
201 | } | |
202 | } | |
203 | ||
204 | return true; | |
205 | } | |
206 | ||
9aadd281 | 207 | static int get_max_brightness(sd_device *device, unsigned *ret) { |
7b909d74 | 208 | const char *max_brightness_str; |
3cadce7d | 209 | unsigned max_brightness; |
9aadd281 | 210 | int r; |
7b909d74 | 211 | |
9aadd281 YW |
212 | assert(device); |
213 | assert(ret); | |
214 | ||
215 | r = sd_device_get_sysattr_value(device, "max_brightness", &max_brightness_str); | |
216 | if (r < 0) | |
87a9a197 | 217 | return log_device_warning_errno(device, r, "Failed to read 'max_brightness' attribute: %m"); |
7b909d74 | 218 | |
3cadce7d | 219 | r = safe_atou(max_brightness_str, &max_brightness); |
9aadd281 | 220 | if (r < 0) |
87a9a197 | 221 | return log_device_warning_errno(device, r, "Failed to parse 'max_brightness' \"%s\": %m", max_brightness_str); |
c7fdf44d LP |
222 | |
223 | if (max_brightness <= 0) { | |
87a9a197 | 224 | log_device_warning(device, "Maximum brightness is 0, ignoring device."); |
9aadd281 | 225 | return -EINVAL; |
7b909d74 JT |
226 | } |
227 | ||
9aadd281 YW |
228 | *ret = max_brightness; |
229 | return 0; | |
3cadce7d TB |
230 | } |
231 | ||
232 | /* Some systems turn the backlight all the way off at the lowest levels. | |
233 | * clamp_brightness clamps the saved brightness to at least 1 or 5% of | |
4cd2b2cf DT |
234 | * max_brightness in case of 'backlight' subsystem. This avoids preserving |
235 | * an unreadably dim screen, which would otherwise force the user to | |
236 | * disable state restoration. */ | |
9aadd281 | 237 | static int clamp_brightness(sd_device *device, char **value, unsigned max_brightness) { |
0c9d8f1d | 238 | unsigned brightness, new_brightness, min_brightness; |
4cd2b2cf | 239 | const char *subsystem; |
3e44d24d | 240 | int r; |
3cadce7d | 241 | |
9aadd281 YW |
242 | assert(value); |
243 | assert(*value); | |
244 | ||
3cadce7d | 245 | r = safe_atou(*value, &brightness); |
9aadd281 | 246 | if (r < 0) |
87a9a197 | 247 | return log_device_warning_errno(device, r, "Failed to parse brightness \"%s\": %m", *value); |
7b909d74 | 248 | |
9aadd281 YW |
249 | r = sd_device_get_subsystem(device, &subsystem); |
250 | if (r < 0) | |
87a9a197 | 251 | return log_device_warning_errno(device, r, "Failed to get device subsystem: %m"); |
9aadd281 YW |
252 | |
253 | if (streq(subsystem, "backlight")) | |
4cd2b2cf DT |
254 | min_brightness = MAX(1U, max_brightness/20); |
255 | else | |
256 | min_brightness = 0; | |
257 | ||
0c9d8f1d | 258 | new_brightness = CLAMP(brightness, min_brightness, max_brightness); |
7b909d74 | 259 | if (new_brightness != brightness) { |
9aadd281 | 260 | char *new_value; |
7b909d74 | 261 | |
9aadd281 YW |
262 | r = asprintf(&new_value, "%u", new_brightness); |
263 | if (r < 0) | |
264 | return log_oom(); | |
7b909d74 | 265 | |
87a9a197 YW |
266 | log_device_info(device, "Saved brightness %s %s to %s.", *value, |
267 | new_brightness > brightness ? | |
268 | "too low; increasing" : "too high; decreasing", | |
269 | new_value); | |
0c9d8f1d | 270 | |
9aadd281 | 271 | free_and_replace(*value, new_value); |
7b909d74 | 272 | } |
9aadd281 YW |
273 | |
274 | return 0; | |
7b909d74 JT |
275 | } |
276 | ||
9aadd281 | 277 | static bool shall_clamp(sd_device *d) { |
4432b941 SR |
278 | const char *s; |
279 | int r; | |
280 | ||
281 | assert(d); | |
282 | ||
9aadd281 | 283 | r = sd_device_get_property_value(d, "ID_BACKLIGHT_CLAMP", &s); |
87a9a197 YW |
284 | if (r < 0) { |
285 | log_device_debug_errno(d, r, "Failed to get ID_BACKLIGHT_CLAMP property, ignoring: %m"); | |
4432b941 | 286 | return true; |
87a9a197 | 287 | } |
4432b941 SR |
288 | |
289 | r = parse_boolean(s); | |
290 | if (r < 0) { | |
87a9a197 | 291 | log_device_debug_errno(d, r, "Failed to parse ID_BACKLIGHT_CLAMP property, ignoring: %m"); |
4432b941 SR |
292 | return true; |
293 | } | |
294 | ||
295 | return r; | |
296 | } | |
297 | ||
1ddae15a | 298 | static int run(int argc, char *argv[]) { |
9aadd281 | 299 | _cleanup_(sd_device_unrefp) sd_device *device = NULL; |
3e44d24d LP |
300 | _cleanup_free_ char *escaped_ss = NULL, *escaped_sysname = NULL, *escaped_path_id = NULL; |
301 | const char *sysname, *path_id, *ss, *saved; | |
3cadce7d | 302 | unsigned max_brightness; |
3731acf1 LP |
303 | int r; |
304 | ||
305 | if (argc != 3) { | |
306 | log_error("This program requires two arguments."); | |
1ddae15a | 307 | return -EINVAL; |
3731acf1 LP |
308 | } |
309 | ||
6bf3c61c | 310 | log_setup_service(); |
3731acf1 LP |
311 | |
312 | umask(0022); | |
313 | ||
ef5bfcf6 | 314 | r = mkdir_p("/var/lib/systemd/backlight", 0755); |
1ddae15a YW |
315 | if (r < 0) |
316 | return log_error_errno(r, "Failed to create backlight directory /var/lib/systemd/backlight: %m"); | |
3731acf1 | 317 | |
0f4ba83c LP |
318 | sysname = strchr(argv[2], ':'); |
319 | if (!sysname) { | |
938d2699 | 320 | log_error("Requires a subsystem and sysname pair specifying a backlight device."); |
1ddae15a | 321 | return -EINVAL; |
0f4ba83c LP |
322 | } |
323 | ||
3e44d24d | 324 | ss = strndupa(argv[2], sysname - argv[2]); |
0f4ba83c LP |
325 | |
326 | sysname++; | |
327 | ||
3e44d24d | 328 | if (!STR_IN_SET(ss, "backlight", "leds")) { |
0f4ba83c | 329 | log_error("Not a backlight or LED device: '%s:%s'", ss, sysname); |
1ddae15a | 330 | return -EINVAL; |
0f4ba83c LP |
331 | } |
332 | ||
9aadd281 | 333 | r = sd_device_new_from_subsystem_sysname(&device, ss, sysname); |
1ddae15a YW |
334 | if (r < 0) |
335 | return log_error_errno(r, "Failed to get backlight or LED device '%s:%s': %m", ss, sysname); | |
3731acf1 | 336 | |
3cadce7d TB |
337 | /* If max_brightness is 0, then there is no actual backlight |
338 | * device. This happens on desktops with Asus mainboards | |
9aadd281 | 339 | * that load the eeepc-wmi module. */ |
1ddae15a YW |
340 | if (get_max_brightness(device, &max_brightness) < 0) |
341 | return 0; | |
3cadce7d | 342 | |
be3f52f4 | 343 | escaped_ss = cescape(ss); |
1ddae15a YW |
344 | if (!escaped_ss) |
345 | return log_oom(); | |
be3f52f4 LP |
346 | |
347 | escaped_sysname = cescape(sysname); | |
1ddae15a YW |
348 | if (!escaped_sysname) |
349 | return log_oom(); | |
be3f52f4 | 350 | |
9aadd281 | 351 | if (sd_device_get_property_value(device, "ID_PATH", &path_id) >= 0) { |
be3f52f4 | 352 | escaped_path_id = cescape(path_id); |
1ddae15a YW |
353 | if (!escaped_path_id) |
354 | return log_oom(); | |
be3f52f4 | 355 | |
3e44d24d | 356 | saved = strjoina("/var/lib/systemd/backlight/", escaped_path_id, ":", escaped_ss, ":", escaped_sysname); |
be3f52f4 | 357 | } else |
3e44d24d | 358 | saved = strjoina("/var/lib/systemd/backlight/", escaped_ss, ":", escaped_sysname); |
3731acf1 | 359 | |
0f4ba83c LP |
360 | /* If there are multiple conflicting backlight devices, then |
361 | * their probing at boot-time might happen in any order. This | |
362 | * means the validity checking of the device then is not | |
363 | * reliable, since it might not see other devices conflicting | |
73e231ab | 364 | * with a specific backlight. To deal with this, we will |
0f4ba83c LP |
365 | * actively delete backlight state files at shutdown (where |
366 | * device probing should be complete), so that the validity | |
367 | * check at boot time doesn't have to be reliable. */ | |
368 | ||
b76388e1 | 369 | if (streq(argv[1], "load")) { |
3731acf1 | 370 | _cleanup_free_ char *value = NULL; |
4432b941 | 371 | bool clamp; |
3731acf1 | 372 | |
934ae16b | 373 | if (shall_restore_state() == 0) |
1ddae15a | 374 | return 0; |
b76388e1 | 375 | |
9aadd281 | 376 | if (validate_device(device) == 0) |
1ddae15a | 377 | return 0; |
0f4ba83c | 378 | |
4432b941 SR |
379 | clamp = shall_clamp(device); |
380 | ||
3731acf1 | 381 | r = read_one_line_file(saved, &value); |
cbed254f | 382 | if (IN_SET(r, -ENOENT, 0)) { |
4432b941 | 383 | const char *curval; |
3731acf1 | 384 | |
4432b941 SR |
385 | /* Fallback to clamping current brightness or exit early if |
386 | * clamping is not supported/enabled. */ | |
387 | if (!clamp) | |
1ddae15a | 388 | return 0; |
3731acf1 | 389 | |
9aadd281 | 390 | r = sd_device_get_sysattr_value(device, "brightness", &curval); |
1ddae15a YW |
391 | if (r < 0) |
392 | return log_device_warning_errno(device, r, "Failed to read 'brightness' attribute: %m"); | |
4432b941 SR |
393 | |
394 | value = strdup(curval); | |
1ddae15a YW |
395 | if (!value) |
396 | return log_oom(); | |
397 | } else if (r < 0) | |
398 | return log_error_errno(r, "Failed to read %s: %m", saved); | |
3731acf1 | 399 | |
4432b941 | 400 | if (clamp) |
9aadd281 | 401 | (void) clamp_brightness(device, &value, max_brightness); |
7b909d74 | 402 | |
9aadd281 | 403 | r = sd_device_set_sysattr_value(device, "brightness", value); |
1ddae15a YW |
404 | if (r < 0) |
405 | return log_device_error_errno(device, r, "Failed to write system 'brightness' attribute: %m"); | |
3731acf1 LP |
406 | |
407 | } else if (streq(argv[1], "save")) { | |
408 | const char *value; | |
409 | ||
9aadd281 | 410 | if (validate_device(device) == 0) { |
0f4ba83c | 411 | unlink(saved); |
1ddae15a | 412 | return 0; |
0f4ba83c LP |
413 | } |
414 | ||
9aadd281 | 415 | r = sd_device_get_sysattr_value(device, "brightness", &value); |
1ddae15a YW |
416 | if (r < 0) |
417 | return log_device_error_errno(device, r, "Failed to read system 'brightness' attribute: %m"); | |
3731acf1 | 418 | |
4c1fc3e4 | 419 | r = write_string_file(saved, value, WRITE_STRING_FILE_CREATE); |
1ddae15a YW |
420 | if (r < 0) |
421 | return log_device_error_errno(device, r, "Failed to write %s: %m", saved); | |
3731acf1 LP |
422 | |
423 | } else { | |
424 | log_error("Unknown verb %s.", argv[1]); | |
1ddae15a | 425 | return -EINVAL; |
3731acf1 LP |
426 | } |
427 | ||
1ddae15a | 428 | return 0; |
3731acf1 | 429 | } |
1ddae15a YW |
430 | |
431 | DEFINE_MAIN_FUNCTION(run); |