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 
event_closep(EFI_EVENT * event)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. */
console_key_read(UINT64 * key,UINT64 timeout_usec)40 EFI_STATUS console_key_read(UINT64 *key, UINT64 timeout_usec) {
41         static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *conInEx = NULL, *extraInEx = NULL;
42         static BOOLEAN 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 = LibLocateProtocol(&SimpleTextInputExProtocol, (void **) &extraInEx);
52                 if (EFI_ERROR(err) || 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 (EFI_ERROR(err) || 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 (EFI_ERROR(err))
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 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 (EFI_ERROR(err))
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 (EFI_ERROR(err))
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 && !EFI_ERROR(BS->CheckEvent(extraInEx->WaitForKeyEx))) {
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 shift = 0;
128 
129                 err = conInEx->ReadKeyStrokeEx(conInEx, &keydata);
130                 if (EFI_ERROR(err))
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_SHIFT_PRESSED)
136                                 shift |= EFI_SHIFT_PRESSED;
137                         if (keydata.KeyState.KeyShiftState & EFI_CONTROL_PRESSED)
138                                 shift |= EFI_CONTROL_PRESSED;
139                         if (keydata.KeyState.KeyShiftState & EFI_ALT_PRESSED)
140                                 shift |= EFI_ALT_PRESSED;
141                         if (keydata.KeyState.KeyShiftState & EFI_LOGO_PRESSED)
142                                 shift |= EFI_LOGO_PRESSED;
143                 }
144 
145                 /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */
146                 *key = KEYPRESS(shift, keydata.Key.ScanCode, keydata.Key.UnicodeChar);
147                 return EFI_SUCCESS;
148         } else if (!EFI_ERROR(BS->CheckEvent(ST->ConIn->WaitForKey))) {
149                 EFI_INPUT_KEY k;
150 
151                 err = ST->ConIn->ReadKeyStroke(ST->ConIn, &k);
152                 if (EFI_ERROR(err))
153                         return err;
154 
155                 *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar);
156                 return EFI_SUCCESS;
157         }
158 
159         return EFI_NOT_READY;
160 }
161 
change_mode(INT64 mode)162 static EFI_STATUS change_mode(INT64 mode) {
163         EFI_STATUS err;
164         INT32 old_mode;
165 
166         /* SetMode expects a UINTN, so make sure these values are sane. */
167         mode = CLAMP(mode, CONSOLE_MODE_RANGE_MIN, CONSOLE_MODE_RANGE_MAX);
168         old_mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
169 
170         err = ST->ConOut->SetMode(ST->ConOut, mode);
171         if (!EFI_ERROR(err))
172                 return EFI_SUCCESS;
173 
174         /* Something went wrong. Output is probably borked, so try to revert to previous mode. */
175         if (!EFI_ERROR(ST->ConOut->SetMode(ST->ConOut, old_mode)))
176                 return err;
177 
178         /* Maybe the device is on fire? */
179         ST->ConOut->Reset(ST->ConOut, TRUE);
180         ST->ConOut->SetMode(ST->ConOut, CONSOLE_MODE_RANGE_MIN);
181         return err;
182 }
183 
query_screen_resolution(UINT32 * ret_w,UINT32 * ret_h)184 EFI_STATUS query_screen_resolution(UINT32 *ret_w, UINT32 *ret_h) {
185         EFI_STATUS err;
186         EFI_GRAPHICS_OUTPUT_PROTOCOL *go;
187 
188         err = LibLocateProtocol(&GraphicsOutputProtocol, (void **) &go);
189         if (EFI_ERROR(err))
190                 return err;
191 
192         if (!go->Mode || !go->Mode->Info)
193                 return EFI_DEVICE_ERROR;
194 
195         *ret_w = go->Mode->Info->HorizontalResolution;
196         *ret_h = go->Mode->Info->VerticalResolution;
197         return EFI_SUCCESS;
198 }
199 
get_auto_mode(void)200 static INT64 get_auto_mode(void) {
201         UINT32 screen_width, screen_height;
202 
203         if (!EFI_ERROR(query_screen_resolution(&screen_width, &screen_height))) {
204                 BOOLEAN keep = FALSE;
205 
206                 /* Start verifying if we are in a resolution larger than Full HD
207                  * (1920x1080). If we're not, assume we're in a good mode and do not
208                  * try to change it. */
209                 if (screen_width <= HORIZONTAL_MAX_OK && screen_height <= VERTICAL_MAX_OK)
210                         keep = TRUE;
211                 /* For larger resolutions, calculate the ratio of the total screen
212                  * area to the text viewport area. If it's less than 10 times bigger,
213                  * then assume the text is readable and keep the text mode. */
214                 else {
215                         UINT64 text_area;
216                         UINTN x_max, y_max;
217                         UINT64 screen_area = (UINT64)screen_width * (UINT64)screen_height;
218 
219                         console_query_mode(&x_max, &y_max);
220                         text_area = SYSTEM_FONT_WIDTH * SYSTEM_FONT_HEIGHT * (UINT64)x_max * (UINT64)y_max;
221 
222                         if (text_area != 0 && screen_area/text_area < VIEWPORT_RATIO)
223                                 keep = TRUE;
224                 }
225 
226                 if (keep)
227                         return ST->ConOut->Mode->Mode;
228         }
229 
230         /* If we reached here, then we have a high resolution screen and the text
231          * viewport is less than 10% the screen area, so the firmware developer
232          * screwed up. Try to switch to a better mode. Mode number 2 is first non
233          * standard mode, which is provided by the device manufacturer, so it should
234          * be a good mode.
235          * Note: MaxMode is the number of modes, not the last mode. */
236         if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_FIRMWARE_FIRST)
237                 return CONSOLE_MODE_FIRMWARE_FIRST;
238 
239         /* Try again with mode different than zero (assume user requests
240          * auto mode due to some problem with mode zero). */
241         if (ST->ConOut->Mode->MaxMode > CONSOLE_MODE_80_50)
242                 return CONSOLE_MODE_80_50;
243 
244         return CONSOLE_MODE_80_25;
245 }
246 
console_set_mode(INT64 mode)247 EFI_STATUS console_set_mode(INT64 mode) {
248         switch (mode) {
249         case CONSOLE_MODE_KEEP:
250                 /* If the firmware indicates the current mode is invalid, change it anyway. */
251                 if (ST->ConOut->Mode->Mode < CONSOLE_MODE_RANGE_MIN)
252                         return change_mode(CONSOLE_MODE_RANGE_MIN);
253                 return EFI_SUCCESS;
254 
255         case CONSOLE_MODE_NEXT:
256                 if (ST->ConOut->Mode->MaxMode <= CONSOLE_MODE_RANGE_MIN)
257                         return EFI_UNSUPPORTED;
258 
259                 mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
260                 do {
261                         mode = (mode + 1) % ST->ConOut->Mode->MaxMode;
262                         if (!EFI_ERROR(change_mode(mode)))
263                                 break;
264                         /* If this mode is broken/unsupported, try the next.
265                          * If mode is 0, we wrapped around and should stop. */
266                 } while (mode > CONSOLE_MODE_RANGE_MIN);
267 
268                 return EFI_SUCCESS;
269 
270         case CONSOLE_MODE_AUTO:
271                 return change_mode(get_auto_mode());
272 
273         case CONSOLE_MODE_FIRMWARE_MAX:
274                 /* Note: MaxMode is the number of modes, not the last mode. */
275                 return change_mode(ST->ConOut->Mode->MaxMode - 1LL);
276 
277         default:
278                 return change_mode(mode);
279         }
280 }
281 
console_query_mode(UINTN * x_max,UINTN * y_max)282 EFI_STATUS console_query_mode(UINTN *x_max, UINTN *y_max) {
283         EFI_STATUS err;
284 
285         assert(x_max);
286         assert(y_max);
287 
288         err = ST->ConOut->QueryMode(ST->ConOut, ST->ConOut->Mode->Mode, x_max, y_max);
289         if (EFI_ERROR(err)) {
290                 /* Fallback values mandated by UEFI spec. */
291                 switch (ST->ConOut->Mode->Mode) {
292                 case CONSOLE_MODE_80_50:
293                         *x_max = 80;
294                         *y_max = 50;
295                         break;
296                 case CONSOLE_MODE_80_25:
297                 default:
298                         *x_max = 80;
299                         *y_max = 25;
300                 }
301         }
302 
303         return err;
304 }
305