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