]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/boot/efi/console.c
man: Remove OSConfig project mentioning for systemd-confext
[thirdparty/systemd.git] / src / boot / efi / console.c
CommitLineData
db9ecf05 1/* SPDX-License-Identifier: LGPL-2.1-or-later */
0fa2cac4 2
0fa2cac4 3#include "console.h"
5080a60a 4#include "proto/graphics-output.h"
cf0fbc49 5#include "util.h"
0fa2cac4 6
26e51d87
HDA
7#define SYSTEM_FONT_WIDTH 8
8#define SYSTEM_FONT_HEIGHT 19
134144ab
JJ
9#define HORIZONTAL_MAX_OK 1920
10#define VERTICAL_MAX_OK 1080
11#define VIEWPORT_RATIO 10
26e51d87 12
cf1ab844 13static void event_closep(EFI_EVENT *event) {
e98d271e
JJ
14 if (!*event)
15 return;
16
12f32748 17 BS->CloseEvent(*event);
e98d271e
JJ
18}
19
20/*
21 * Reading input from the console sounds like an easy task to do, but thanks to broken
22 * firmware it is actually a nightmare.
23 *
e628944b
JJ
24 * There is a SimpleTextInput and SimpleTextInputEx API for this. Ideally we want to use
25 * TextInputEx, because that gives us Ctrl/Alt/Shift key state information. Unfortunately,
26 * it is not always available and sometimes just non-functional.
e98d271e 27 *
e628944b
JJ
28 * On some firmware, calling ReadKeyStroke or ReadKeyStrokeEx on the default console input
29 * device will just freeze no matter what (even though it *reported* being ready).
30 * Also, multiple input protocols can be backed by the same device, but they can be out of
31 * sync. Falling back on a different protocol can end up with double input.
e98d271e 32 *
3e7bf853 33 * Therefore, we will preferably use TextInputEx for ConIn if that is available. Additionally,
e628944b
JJ
34 * we look for the first TextInputEx device the firmware gives us as a fallback option. It
35 * will replace ConInEx permanently if it ever reports a key press.
36 * Lastly, a timer event allows us to provide a input timeout without having to call into
37 * any input functions that can freeze on us or using a busy/stall loop. */
db4122d1 38EFI_STATUS console_key_read(uint64_t *key, uint64_t timeout_usec) {
e628944b 39 static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *conInEx = NULL, *extraInEx = NULL;
e5a1b8f9 40 static bool checked = false;
dede50a7 41 size_t index;
0fa2cac4 42 EFI_STATUS err;
0b2281a2 43 _cleanup_(event_closep) EFI_EVENT timer = NULL;
0fa2cac4 44
508df915
JJ
45 assert(key);
46
0fa2cac4 47 if (!checked) {
0b75493d 48 /* Get the *first* TextInputEx device. */
19f08504
JJ
49 err = BS->LocateProtocol(
50 MAKE_GUID_PTR(EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL), NULL, (void **) &extraInEx);
2a5e4fe4 51 if (err != EFI_SUCCESS || BS->CheckEvent(extraInEx->WaitForKeyEx) == EFI_INVALID_PARAMETER)
e98d271e
JJ
52 /* If WaitForKeyEx fails here, the firmware pretends it talks this
53 * protocol, but it really doesn't. */
e628944b
JJ
54 extraInEx = NULL;
55
56 /* Get the TextInputEx version of ST->ConIn. */
19f08504
JJ
57 err = BS->HandleProtocol(
58 ST->ConsoleInHandle,
59 MAKE_GUID_PTR(EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL),
60 (void **) &conInEx);
2a5e4fe4 61 if (err != EFI_SUCCESS || BS->CheckEvent(conInEx->WaitForKeyEx) == EFI_INVALID_PARAMETER)
e628944b
JJ
62 conInEx = NULL;
63
64 if (conInEx == extraInEx)
65 extraInEx = NULL;
66
e5a1b8f9 67 checked = true;
0fa2cac4
KS
68 }
69
12f32748 70 err = BS->CreateEvent(EVT_TIMER, 0, NULL, NULL, &timer);
2a5e4fe4 71 if (err != EFI_SUCCESS)
c2c62035 72 return log_error_status(err, "Error creating timer event: %m");
e628944b
JJ
73
74 EFI_EVENT events[] = {
75 timer,
76 conInEx ? conInEx->WaitForKeyEx : ST->ConIn->WaitForKey,
77 extraInEx ? extraInEx->WaitForKeyEx : NULL,
78 };
dede50a7 79 size_t n_events = extraInEx ? 3 : 2;
6ae9a5a0
JJ
80
81 /* Watchdog rearming loop in case the user never provides us with input or some
82 * broken firmware never returns from WaitForEvent. */
83 for (;;) {
db4122d1 84 uint64_t watchdog_timeout_sec = 5 * 60,
6ae9a5a0 85 watchdog_ping_usec = watchdog_timeout_sec / 2 * 1000 * 1000;
e98d271e
JJ
86
87 /* SetTimer expects 100ns units for some reason. */
12f32748 88 err = BS->SetTimer(
6ae9a5a0
JJ
89 timer,
90 TimerRelative,
91 MIN(timeout_usec, watchdog_ping_usec) * 10);
2a5e4fe4 92 if (err != EFI_SUCCESS)
c2c62035 93 return log_error_status(err, "Error arming timer event: %m");
e98d271e 94
12f32748
JJ
95 (void) BS->SetWatchdogTimer(watchdog_timeout_sec, 0x10000, 0, NULL);
96 err = BS->WaitForEvent(n_events, events, &index);
97 (void) BS->SetWatchdogTimer(watchdog_timeout_sec, 0x10000, 0, NULL);
e98d271e 98
2a5e4fe4 99 if (err != EFI_SUCCESS)
c2c62035 100 return log_error_status(err, "Error waiting for events: %m");
6ae9a5a0
JJ
101
102 /* We have keyboard input, process it after this loop. */
103 if (timer != events[index])
104 break;
0fa2cac4 105
6ae9a5a0
JJ
106 /* The EFI timer fired instead. If this was a watchdog timeout, loop again. */
107 if (timeout_usec == UINT64_MAX)
108 continue;
109 else if (timeout_usec > watchdog_ping_usec) {
110 timeout_usec -= watchdog_ping_usec;
111 continue;
112 }
113
114 /* The caller requested a timeout? They shall have one! */
e98d271e 115 return EFI_TIMEOUT;
6ae9a5a0 116 }
e98d271e 117
e628944b
JJ
118 /* If the extra input device we found returns something, always use that instead
119 * to work around broken firmware freezing on ConIn/ConInEx. */
2a5e4fe4 120 if (extraInEx && BS->CheckEvent(extraInEx->WaitForKeyEx) == EFI_SUCCESS) {
e628944b
JJ
121 conInEx = extraInEx;
122 extraInEx = NULL;
123 }
124
125 /* Do not fall back to ConIn if we have a ConIn that supports TextInputEx.
126 * The two may be out of sync on some firmware, giving us double input. */
127 if (conInEx) {
0fa2cac4 128 EFI_KEY_DATA keydata;
db4122d1 129 uint32_t shift = 0;
0fa2cac4 130
e628944b 131 err = conInEx->ReadKeyStrokeEx(conInEx, &keydata);
2a5e4fe4 132 if (err != EFI_SUCCESS)
e98d271e
JJ
133 return err;
134
3f9973bf
JJ
135 if (FLAGS_SET(keydata.KeyState.KeyShiftState, EFI_SHIFT_STATE_VALID)) {
136 /* Do not distinguish between left and right keys (set both flags). */
3f9973bf 137 if (keydata.KeyState.KeyShiftState & EFI_CONTROL_PRESSED)
e98d271e 138 shift |= EFI_CONTROL_PRESSED;
3f9973bf 139 if (keydata.KeyState.KeyShiftState & EFI_ALT_PRESSED)
e98d271e 140 shift |= EFI_ALT_PRESSED;
3f9973bf
JJ
141 if (keydata.KeyState.KeyShiftState & EFI_LOGO_PRESSED)
142 shift |= EFI_LOGO_PRESSED;
d17d0e67
JJ
143
144 /* Shift is not supposed to be reported for keys that can be represented as uppercase
145 * unicode chars (Shift+f is reported as F instead). Some firmware does it anyway, so
146 * filter those out. */
147 if ((keydata.KeyState.KeyShiftState & EFI_SHIFT_PRESSED) &&
148 keydata.Key.UnicodeChar == 0)
149 shift |= EFI_SHIFT_PRESSED;
e628944b 150 }
e98d271e
JJ
151
152 /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */
3f9973bf
JJ
153 *key = KEYPRESS(shift, keydata.Key.ScanCode, keydata.Key.UnicodeChar);
154 return EFI_SUCCESS;
2a5e4fe4 155 } else if (BS->CheckEvent(ST->ConIn->WaitForKey) == EFI_SUCCESS) {
e628944b 156 EFI_INPUT_KEY k;
0fa2cac4 157
e628944b 158 err = ST->ConIn->ReadKeyStroke(ST->ConIn, &k);
2a5e4fe4 159 if (err != EFI_SUCCESS)
e628944b
JJ
160 return err;
161
162 *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar);
163 return EFI_SUCCESS;
164 }
0fa2cac4 165
e628944b 166 return EFI_NOT_READY;
0fa2cac4 167}
68d4b8ac 168
db4122d1 169static EFI_STATUS change_mode(int64_t mode) {
68d4b8ac 170 EFI_STATUS err;
db4122d1 171 int32_t old_mode;
134144ab 172
dede50a7 173 /* SetMode expects a size_t, so make sure these values are sane. */
134144ab
JJ
174 mode = CLAMP(mode, CONSOLE_MODE_RANGE_MIN, CONSOLE_MODE_RANGE_MAX);
175 old_mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
68d4b8ac 176
6ac54809 177 log_wait();
12f32748 178 err = ST->ConOut->SetMode(ST->ConOut, mode);
2a5e4fe4 179 if (err == EFI_SUCCESS)
134144ab 180 return EFI_SUCCESS;
68d4b8ac 181
134144ab 182 /* Something went wrong. Output is probably borked, so try to revert to previous mode. */
2a5e4fe4 183 if (ST->ConOut->SetMode(ST->ConOut, old_mode) == EFI_SUCCESS)
134144ab 184 return err;
68d4b8ac 185
134144ab 186 /* Maybe the device is on fire? */
e5a1b8f9 187 ST->ConOut->Reset(ST->ConOut, true);
12f32748 188 ST->ConOut->SetMode(ST->ConOut, CONSOLE_MODE_RANGE_MIN);
68d4b8ac
HDA
189 return err;
190}
191
db4122d1 192EFI_STATUS query_screen_resolution(uint32_t *ret_w, uint32_t *ret_h) {
26e51d87 193 EFI_STATUS err;
1926a114
JJ
194 EFI_GRAPHICS_OUTPUT_PROTOCOL *go;
195
19f08504 196 err = BS->LocateProtocol(MAKE_GUID_PTR(EFI_GRAPHICS_OUTPUT_PROTOCOL), NULL, (void **) &go);
2a5e4fe4 197 if (err != EFI_SUCCESS)
1926a114
JJ
198 return err;
199
200 if (!go->Mode || !go->Mode->Info)
201 return EFI_DEVICE_ERROR;
202
203 *ret_w = go->Mode->Info->HorizontalResolution;
204 *ret_h = go->Mode->Info->VerticalResolution;
205 return EFI_SUCCESS;
206}
207
db4122d1
JJ
208static int64_t get_auto_mode(void) {
209 uint32_t screen_width, screen_height;
26e51d87 210
2a5e4fe4 211 if (query_screen_resolution(&screen_width, &screen_height) == EFI_SUCCESS) {
e5a1b8f9 212 bool keep = false;
26e51d87
HDA
213
214 /* Start verifying if we are in a resolution larger than Full HD
215 * (1920x1080). If we're not, assume we're in a good mode and do not
216 * try to change it. */
1926a114 217 if (screen_width <= HORIZONTAL_MAX_OK && screen_height <= VERTICAL_MAX_OK)
e5a1b8f9 218 keep = true;
26e51d87
HDA
219 /* For larger resolutions, calculate the ratio of the total screen
220 * area to the text viewport area. If it's less than 10 times bigger,
221 * then assume the text is readable and keep the text mode. */
222 else {
db4122d1 223 uint64_t text_area;
dede50a7 224 size_t x_max, y_max;
db4122d1 225 uint64_t screen_area = (uint64_t)screen_width * (uint64_t)screen_height;
134144ab
JJ
226
227 console_query_mode(&x_max, &y_max);
db4122d1 228 text_area = SYSTEM_FONT_WIDTH * SYSTEM_FONT_HEIGHT * (uint64_t)x_max * (uint64_t)y_max;
26e51d87
HDA
229
230 if (text_area != 0 && screen_area/text_area < VIEWPORT_RATIO)
e5a1b8f9 231 keep = true;
26e51d87 232 }
26e51d87 233
134144ab
JJ
234 if (keep)
235 return ST->ConOut->Mode->Mode;
26e51d87
HDA
236 }
237
238 /* If we reached here, then we have a high resolution screen and the text
239 * viewport is less than 10% the screen area, so the firmware developer
240 * screwed up. Try to switch to a better mode. Mode number 2 is first non
241 * standard mode, which is provided by the device manufacturer, so it should
242 * be a good mode.
68d4b8ac 243 * Note: MaxMode is the number of modes, not the last mode. */
134144ab
JJ
244 if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_FIRMWARE_FIRST)
245 return CONSOLE_MODE_FIRMWARE_FIRST;
246
68d4b8ac
HDA
247 /* Try again with mode different than zero (assume user requests
248 * auto mode due to some problem with mode zero). */
134144ab
JJ
249 if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_80_50)
250 return CONSOLE_MODE_80_50;
68d4b8ac 251
134144ab 252 return CONSOLE_MODE_80_25;
68d4b8ac
HDA
253}
254
db4122d1 255EFI_STATUS console_set_mode(int64_t mode) {
134144ab
JJ
256 switch (mode) {
257 case CONSOLE_MODE_KEEP:
258 /* If the firmware indicates the current mode is invalid, change it anyway. */
259 if (ST->ConOut->Mode->Mode < CONSOLE_MODE_RANGE_MIN)
260 return change_mode(CONSOLE_MODE_RANGE_MIN);
261 return EFI_SUCCESS;
508df915 262
1b965abc
JJ
263 case CONSOLE_MODE_NEXT:
264 if (ST->ConOut->Mode->MaxMode <= CONSOLE_MODE_RANGE_MIN)
265 return EFI_UNSUPPORTED;
266
267 mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
268 do {
269 mode = (mode + 1) % ST->ConOut->Mode->MaxMode;
2a5e4fe4 270 if (change_mode(mode) == EFI_SUCCESS)
1b965abc
JJ
271 break;
272 /* If this mode is broken/unsupported, try the next.
273 * If mode is 0, we wrapped around and should stop. */
274 } while (mode > CONSOLE_MODE_RANGE_MIN);
275
276 return EFI_SUCCESS;
277
134144ab
JJ
278 case CONSOLE_MODE_AUTO:
279 return change_mode(get_auto_mode());
68d4b8ac 280
134144ab 281 case CONSOLE_MODE_FIRMWARE_MAX:
68d4b8ac 282 /* Note: MaxMode is the number of modes, not the last mode. */
134144ab
JJ
283 return change_mode(ST->ConOut->Mode->MaxMode - 1LL);
284
285 default:
286 return change_mode(mode);
287 }
288}
289
dede50a7 290EFI_STATUS console_query_mode(size_t *x_max, size_t *y_max) {
134144ab
JJ
291 EFI_STATUS err;
292
293 assert(x_max);
294 assert(y_max);
295
12f32748 296 err = ST->ConOut->QueryMode(ST->ConOut, ST->ConOut->Mode->Mode, x_max, y_max);
2a5e4fe4 297 if (err != EFI_SUCCESS) {
134144ab
JJ
298 /* Fallback values mandated by UEFI spec. */
299 switch (ST->ConOut->Mode->Mode) {
300 case CONSOLE_MODE_80_50:
301 *x_max = 80;
302 *y_max = 50;
303 break;
304 case CONSOLE_MODE_80_25:
305 default:
306 *x_max = 80;
307 *y_max = 25;
308 }
68d4b8ac
HDA
309 }
310
134144ab 311 return err;
68d4b8ac 312}