]>
Commit | Line | Data |
---|---|---|
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 | 13 | static 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 | 38 | EFI_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 | 169 | static 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 | 192 | EFI_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 |
208 | static 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 | 255 | EFI_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 | 290 | EFI_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 | } |