blob: 2cd3ef028dfc4d4ee41a0ce364b68217c85392f3 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "input.h"
#include <inttypes.h>
#include <limits.h>
#include <log.h>
#include <algorithm>
#include <memory>
#include "efi/types.h"
#include "lib/fit/internal/result.h"
#include "lib/fit/result.h"
#include "utils.h"
namespace gigaboot {
namespace {
// Super basic single-character UTF-16 to ASCII conversion. Anything outside of
// the [0x01, 0x7F] range just gets converted to std::nullopt.
std::optional<char> SimpleUtf16ToAscii(uint16_t utf16) {
return (utf16 & 0xFF80 || !utf16) ? std::nullopt : std::optional(static_cast<char>(utf16 & 0x7F));
}
fit::result<efi_status, char> GetAsciiKeyConIn(efi_system_table* sys) {
efi_input_key key;
efi_status res = sys->ConIn->ReadKeyStroke(sys->ConIn, &key);
if (res != EFI_SUCCESS) {
return fit::error(res);
}
std::optional<char> c = SimpleUtf16ToAscii(key.UnicodeChar);
if (c) {
return fit::ok(*c);
}
return fit::error(EFI_UNSUPPORTED);
}
class Cursor {
public:
Cursor() = delete;
Cursor(const Cursor&) = delete;
Cursor(Cursor&&) = delete;
Cursor& operator=(const Cursor&) = delete;
Cursor& operator=(Cursor&&) = delete;
Cursor(efi_system_table* sys, simple_text_output_mode mode) : sys_(sys), mode_(mode) {}
~Cursor() {
ResetPosition();
SetVisibility(mode_.CursorVisible);
}
// Explicitly not static because we want to force users to go through the constructor as a guard.
// Don't care about the error code either.
void SetVisibility(bool on) const { sys_->ConOut->EnableCursor(sys_->ConOut, on); }
void ResetPosition() const {
sys_->ConOut->SetCursorPosition(sys_->ConOut, mode_.CursorColumn, mode_.CursorRow);
}
private:
efi_system_table* sys_;
simple_text_output_mode mode_;
};
} // namespace
InputReceiver::Serial::Serial() {
auto res = EfiLocateProtocol<efi_serial_io_protocol>();
if (res.is_ok()) {
serial_ = std::move(*res);
if (serial_->Mode) {
mode_ = *serial_->Mode;
}
}
}
InputReceiver::Serial::~Serial() {
if (serial_) {
efi_status res =
serial_->SetAttributes(serial_.get(), mode_.BaudRate, mode_.ReceiveFifoDepth, mode_.Timeout,
mode_.Parity, static_cast<uint8_t>(mode_.DataBits), mode_.StopBits);
if (res != EFI_SUCCESS) {
printf("failed to restore serial attributes: %s\n", EfiStatusToString(res));
}
}
}
fit::result<efi_status> InputReceiver::Serial::SetPoll(zx::duration timeout) {
efi_status res = EFI_SUCCESS;
if (serial_) {
res = serial_->SetAttributes(serial_.get(), mode_.BaudRate, mode_.ReceiveFifoDepth,
static_cast<uint32_t>(timeout.to_usecs()), mode_.Parity,
static_cast<uint8_t>(mode_.DataBits), mode_.StopBits);
}
// If there is no serial, SetPoll is a no-op.
// This makes sense from a caller perspective:
// they don't actually care whether there is a serial device, only that
// if there were one it would behave appropriately.
if (res == EFI_SUCCESS) {
return fit::ok();
}
return fit::error(res);
}
fit::result<efi_status, char> InputReceiver::Serial::GetChar() {
// GetChar is different from SetPoll: an error isn't necessarily unrecoverable,
// it may mean that the poll just timed out.
if (!serial_) {
return fit::error(EFI_UNSUPPORTED);
}
char c;
size_t read_len = 1;
efi_status status = serial_->Read(serial_.get(), &read_len, &c);
if (status == EFI_SUCCESS && read_len == 1 && c != '\0') {
return fit::ok(c);
}
return fit::error((status == EFI_SUCCESS) ? EFI_NOT_READY : status);
}
fit::result<efi_status, char> InputReceiver::GetKey(zx::duration timeout) {
fit::result<efi_status> res = serial_.SetPoll(zx::msec(1));
if (res.is_error()) {
return res.take_error();
}
Timer timer(sys_);
res = timer.SetTimer(TimerRelative, timeout);
if (res.is_error()) {
printf("%s: failed to set timer: %s\n", __func__, EfiStatusToString(res.error_value()));
return res.take_error();
}
// Run the checks at least once so we poll if timeout == 0.
do {
// Console input gets priority, check it first.
fit::result<efi_status, char> c = GetAsciiKeyConIn(sys_);
if (c.is_ok()) {
return c;
}
c = serial_.GetChar();
if (c.is_ok()) {
return c;
}
} while (timer.CheckTimer() == Timer::Status::kWaiting);
return fit::error(EFI_TIMEOUT);
}
std::optional<char> InputReceiver::GetKeyPrompt(std::string_view valid_keys, zx::duration timeout,
std::string_view prompt) {
if (valid_keys.empty()) {
return std::nullopt;
}
Cursor cursor(sys_, (sys_->ConOut->Mode) ? *sys_->ConOut->Mode : simple_text_output_mode());
cursor.SetVisibility(false);
const zx::duration tick = zx::sec(timeout == zx::duration::infinite() ? 0 : 1);
do {
if (!prompt.empty()) {
if (tick != zx::duration(0)) {
LOG("%.*s %" PRIu64 "s", static_cast<int>(prompt.size()), prompt.data(), timeout.to_secs());
} else {
LOG("%.*s", static_cast<int>(prompt.size()), prompt.data());
}
}
fit::result<efi_status, char> key = GetKey(zx::sec(1));
if (key.is_ok() &&
std::find(valid_keys.cbegin(), valid_keys.cend(), *key) != valid_keys.cend()) {
return *key;
}
cursor.ResetPosition();
timeout -= tick;
} while (timeout > zx::sec(0));
return std::nullopt;
}
} // namespace gigaboot