]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/backlight/backlight.c
backlight: warn if kernel exposes backlight device with bogus max_brightness
[thirdparty/systemd.git] / src / backlight / backlight.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4 This file is part of systemd.
5
6 Copyright 2013 Lennart Poettering
7
8 systemd is free software; you can redistribute it and/or modify it
9 under the terms of the GNU Lesser General Public License as published by
10 the Free Software Foundation; either version 2.1 of the License, or
11 (at your option) any later version.
12
13 systemd is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public License
19 along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include "util.h"
23 #include "mkdir.h"
24 #include "fileio.h"
25 #include "libudev.h"
26 #include "udev-util.h"
27 #include "def.h"
28
29 static struct udev_device *find_pci_or_platform_parent(struct udev_device *device) {
30 struct udev_device *parent;
31 const char *subsystem, *sysname;
32
33 assert(device);
34
35 parent = udev_device_get_parent(device);
36 if (!parent)
37 return NULL;
38
39 subsystem = udev_device_get_subsystem(parent);
40 if (!subsystem)
41 return NULL;
42
43 sysname = udev_device_get_sysname(parent);
44 if (!sysname)
45 return NULL;
46
47 if (streq(subsystem, "drm")) {
48 const char *c;
49
50 c = startswith(sysname, "card");
51 if (!c)
52 return NULL;
53
54 c += strspn(c, DIGITS);
55 if (*c == '-') {
56 /* A connector DRM device, let's ignore all but LVDS and eDP! */
57
58 if (!startswith(c, "-LVDS-") &&
59 !startswith(c, "-Embedded DisplayPort-"))
60 return NULL;
61 }
62
63 } else if (streq(subsystem, "pci")) {
64 const char *value;
65
66 value = udev_device_get_sysattr_value(parent, "class");
67 if (value) {
68 unsigned long class = 0;
69
70 if (safe_atolu(value, &class) < 0) {
71 log_warning("Cannot parse PCI class %s of device %s:%s.",
72 value, subsystem, sysname);
73 return NULL;
74 }
75
76 /* Graphics card */
77 if (class == 0x30000)
78 return parent;
79 }
80
81 } else if (streq(subsystem, "platform"))
82 return parent;
83
84 return find_pci_or_platform_parent(parent);
85 }
86
87 static bool same_device(struct udev_device *a, struct udev_device *b) {
88 assert(a);
89 assert(b);
90
91 if (!streq_ptr(udev_device_get_subsystem(a), udev_device_get_subsystem(b)))
92 return false;
93
94 if (!streq_ptr(udev_device_get_sysname(a), udev_device_get_sysname(b)))
95 return false;
96
97 return true;
98 }
99
100 static bool validate_device(struct udev *udev, struct udev_device *device) {
101 _cleanup_udev_enumerate_unref_ struct udev_enumerate *enumerate = NULL;
102 struct udev_list_entry *item = NULL, *first = NULL;
103 struct udev_device *parent;
104 const char *v, *subsystem;
105 int r;
106
107 assert(udev);
108 assert(device);
109
110 /* Verify whether we should actually care for a specific
111 * backlight device. For backlight devices there might be
112 * multiple ways to access the same control: "firmware"
113 * (i.e. ACPI), "platform" (i.e. via the machine's EC) and
114 * "raw" (via the graphics card). In general we should prefer
115 * "firmware" (i.e. ACPI) or "platform" access over "raw"
116 * access, in order not to confuse the BIOS/EC, and
117 * compatibility with possible low-level hotkey handling of
118 * screen brightness. The kernel will already make sure to
119 * expose only one of "firmware" and "platform" for the same
120 * device to userspace. However, we still need to make sure
121 * that we use "raw" only if no "firmware" or "platform"
122 * device for the same device exists. */
123
124 subsystem = udev_device_get_subsystem(device);
125 if (!streq_ptr(subsystem, "backlight"))
126 return true;
127
128 v = udev_device_get_sysattr_value(device, "type");
129 if (!streq_ptr(v, "raw"))
130 return true;
131
132 parent = find_pci_or_platform_parent(device);
133 if (!parent)
134 return true;
135
136 subsystem = udev_device_get_subsystem(parent);
137 if (!subsystem)
138 return true;
139
140 enumerate = udev_enumerate_new(udev);
141 if (!enumerate)
142 return true;
143
144 r = udev_enumerate_add_match_subsystem(enumerate, "backlight");
145 if (r < 0)
146 return true;
147
148 r = udev_enumerate_scan_devices(enumerate);
149 if (r < 0)
150 return true;
151
152 first = udev_enumerate_get_list_entry(enumerate);
153 udev_list_entry_foreach(item, first) {
154 _cleanup_udev_device_unref_ struct udev_device *other;
155 struct udev_device *other_parent;
156 const char *other_subsystem;
157
158 other = udev_device_new_from_syspath(udev, udev_list_entry_get_name(item));
159 if (!other)
160 return true;
161
162 if (same_device(device, other))
163 continue;
164
165 v = udev_device_get_sysattr_value(other, "type");
166 if (!streq_ptr(v, "platform") && !streq_ptr(v, "firmware"))
167 continue;
168
169 /* OK, so there's another backlight device, and it's a
170 * platform or firmware device, so, let's see if we
171 * can verify it belongs to the same device as
172 * ours. */
173 other_parent = find_pci_or_platform_parent(other);
174 if (!other_parent)
175 continue;
176
177 if (same_device(parent, other_parent)) {
178 /* Both have the same PCI parent, that means
179 * we are out. */
180 log_debug("Skipping backlight device %s, since device %s is on same PCI device and takes precedence.",
181 udev_device_get_sysname(device),
182 udev_device_get_sysname(other));
183 return false;
184 }
185
186 other_subsystem = udev_device_get_subsystem(other_parent);
187 if (streq_ptr(other_subsystem, "platform") && streq_ptr(subsystem, "pci")) {
188 /* The other is connected to the platform bus
189 * and we are a PCI device, that also means we
190 * are out. */
191 log_debug("Skipping backlight device %s, since device %s is a platform device and takes precedence.",
192 udev_device_get_sysname(device),
193 udev_device_get_sysname(other));
194 return false;
195 }
196 }
197
198 return true;
199 }
200
201 static unsigned get_max_brightness(struct udev_device *device) {
202 int r;
203 const char *max_brightness_str;
204 unsigned max_brightness;
205
206 max_brightness_str = udev_device_get_sysattr_value(device, "max_brightness");
207 if (!max_brightness_str) {
208 log_warning("Failed to read 'max_brightness' attribute.");
209 return 0;
210 }
211
212 r = safe_atou(max_brightness_str, &max_brightness);
213 if (r < 0) {
214 log_warning("Failed to parse 'max_brightness' \"%s\": %s", max_brightness_str, strerror(-r));
215 return 0;
216 }
217
218 if (max_brightness <= 0) {
219 log_warning("Maximum brightness is 0, ignoring device.");
220 return 0;
221 }
222
223 return max_brightness;
224 }
225
226 /* Some systems turn the backlight all the way off at the lowest levels.
227 * clamp_brightness clamps the saved brightness to at least 1 or 5% of
228 * max_brightness. This avoids preserving an unreadably dim screen, which
229 * would otherwise force the user to disable state restoration. */
230 static void clamp_brightness(struct udev_device *device, char **value, unsigned max_brightness) {
231 int r;
232 unsigned brightness, new_brightness;
233
234 r = safe_atou(*value, &brightness);
235 if (r < 0) {
236 log_warning("Failed to parse brightness \"%s\": %s", *value, strerror(-r));
237 return;
238 }
239
240 new_brightness = MAX3(brightness, 1U, max_brightness/20);
241 if (new_brightness != brightness) {
242 char *old_value = *value;
243
244 r = asprintf(value, "%u", new_brightness);
245 if (r < 0) {
246 log_oom();
247 return;
248 }
249
250 log_debug("Saved brightness %s too low; increasing to %s.", old_value, *value);
251 free(old_value);
252 }
253 }
254
255 int main(int argc, char *argv[]) {
256 _cleanup_udev_unref_ struct udev *udev = NULL;
257 _cleanup_udev_device_unref_ struct udev_device *device = NULL;
258 _cleanup_free_ char *saved = NULL, *ss = NULL, *escaped_ss = NULL, *escaped_sysname = NULL, *escaped_path_id = NULL;
259 const char *sysname, *path_id;
260 unsigned max_brightness;
261 int r;
262
263 if (argc != 3) {
264 log_error("This program requires two arguments.");
265 return EXIT_FAILURE;
266 }
267
268 log_set_target(LOG_TARGET_AUTO);
269 log_parse_environment();
270 log_open();
271
272 umask(0022);
273
274 r = mkdir_p("/var/lib/systemd/backlight", 0755);
275 if (r < 0) {
276 log_error("Failed to create backlight directory /var/lib/systemd/backlight: %s",
277 strerror(-r));
278 return EXIT_FAILURE;
279 }
280
281 udev = udev_new();
282 if (!udev) {
283 log_oom();
284 return EXIT_FAILURE;
285 }
286
287 sysname = strchr(argv[2], ':');
288 if (!sysname) {
289 log_error("Requires a subsystem and sysname pair specifying a backlight device.");
290 return EXIT_FAILURE;
291 }
292
293 ss = strndup(argv[2], sysname - argv[2]);
294 if (!ss) {
295 log_oom();
296 return EXIT_FAILURE;
297 }
298
299 sysname++;
300
301 if (!streq(ss, "backlight") && !streq(ss, "leds")) {
302 log_error("Not a backlight or LED device: '%s:%s'", ss, sysname);
303 return EXIT_FAILURE;
304 }
305
306 errno = 0;
307 device = udev_device_new_from_subsystem_sysname(udev, ss, sysname);
308 if (!device) {
309 if (errno != 0)
310 log_error("Failed to get backlight or LED device '%s:%s': %m", ss, sysname);
311 else
312 log_oom();
313
314 return EXIT_FAILURE;
315 }
316
317 /* If max_brightness is 0, then there is no actual backlight
318 * device. This happens on desktops with Asus mainboards
319 * that load the eeepc-wmi module.
320 */
321 max_brightness = get_max_brightness(device);
322 if (max_brightness == 0)
323 return EXIT_SUCCESS;
324
325 escaped_ss = cescape(ss);
326 if (!escaped_ss) {
327 log_oom();
328 return EXIT_FAILURE;
329 }
330
331 escaped_sysname = cescape(sysname);
332 if (!escaped_sysname) {
333 log_oom();
334 return EXIT_FAILURE;
335 }
336
337 path_id = udev_device_get_property_value(device, "ID_PATH");
338 if (path_id) {
339 escaped_path_id = cescape(path_id);
340 if (!escaped_path_id) {
341 log_oom();
342 return EXIT_FAILURE;
343 }
344
345 saved = strjoin("/var/lib/systemd/backlight/", escaped_path_id, ":", escaped_ss, ":", escaped_sysname, NULL);
346 } else
347 saved = strjoin("/var/lib/systemd/backlight/", escaped_ss, ":", escaped_sysname, NULL);
348
349 if (!saved) {
350 log_oom();
351 return EXIT_FAILURE;
352 }
353
354 /* If there are multiple conflicting backlight devices, then
355 * their probing at boot-time might happen in any order. This
356 * means the validity checking of the device then is not
357 * reliable, since it might not see other devices conflicting
358 * with a specific backlight. To deal with this, we will
359 * actively delete backlight state files at shutdown (where
360 * device probing should be complete), so that the validity
361 * check at boot time doesn't have to be reliable. */
362
363 if (streq(argv[1], "load") && shall_restore_state()) {
364 _cleanup_free_ char *value = NULL;
365
366 if (!validate_device(udev, device))
367 return EXIT_SUCCESS;
368
369 r = read_one_line_file(saved, &value);
370 if (r < 0) {
371
372 if (r == -ENOENT)
373 return EXIT_SUCCESS;
374
375 log_error("Failed to read %s: %s", saved, strerror(-r));
376 return EXIT_FAILURE;
377 }
378
379 clamp_brightness(device, &value, max_brightness);
380
381 r = udev_device_set_sysattr_value(device, "brightness", value);
382 if (r < 0) {
383 log_error("Failed to write system 'brightness' attribute: %s",
384 strerror(-r));
385 return EXIT_FAILURE;
386 }
387
388 } else if (streq(argv[1], "save")) {
389 const char *value;
390
391 if (!validate_device(udev, device)) {
392 unlink(saved);
393 return EXIT_SUCCESS;
394 }
395
396 value = udev_device_get_sysattr_value(device, "brightness");
397 if (!value) {
398 log_error("Failed to read system 'brightness' attribute");
399 return EXIT_FAILURE;
400 }
401
402 r = write_string_file(saved, value);
403 if (r < 0) {
404 log_error("Failed to write %s: %s", saved, strerror(-r));
405 return EXIT_FAILURE;
406 }
407
408 } else {
409 log_error("Unknown verb %s.", argv[1]);
410 return EXIT_FAILURE;
411 }
412
413 return EXIT_SUCCESS;
414 }