blob: 033aeded2abcd206a18be74406b5416d584d49f5 [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "minios/screens/screen_debug_options.h"
#include <linux/input.h>
#include <sys/mount.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/logging.h>
#include <base/strings/to_string.h>
#include <brillo/namespaces/platform.h>
#include <minios/proto_bindings/minios.pb.h>
#include <vpd/vpd.h>
#include "base/command_line.h"
#include "minios/blkid_wrapper.h"
#include "minios/draw_utils.h"
#include "minios/log_store_manager.h"
#include "minios/process_manager.h"
#include "minios/screen_controller.h"
#include "minios/screen_types.h"
#include "minios/utils.h"
namespace minios {
namespace {
constexpr int kItemHeight = 40;
constexpr int kNumButtons = 6;
const base::FilePath kArchiveFileName{"minios_logs.tar"};
typedef std::vector<std::pair<ScreenDebugOptions::ButtonIndex, std::string>>
ButtonList;
constexpr char kEraseLogImage[] = "btn_erase_logs";
constexpr char kMessagLogImage[] = "btn_message_log";
constexpr char kBackImage[] = "btn_back";
const ButtonList kButtonLabels{
{ScreenDebugOptions::ButtonIndex::kEraseLogs, kEraseLogImage},
{ScreenDebugOptions::ButtonIndex::kMessagLog, kMessagLogImage},
{ScreenDebugOptions::ButtonIndex::kBack, kBackImage}};
constexpr char kFilesystemTypeTag[] = "TYPE";
constexpr char kLabelTag[] = "LABEL";
} // namespace
const char kDiskStorageDevice[] = "Disk";
// TODO(b/191139789): minios: clean up, combine generic screens into one.
ScreenDebugOptions::ScreenDebugOptions(
std::shared_ptr<DrawInterface> draw_utils,
std::shared_ptr<LogStoreManagerInterface> log_store_manager,
ScreenControllerInterface* screen_controller,
std::shared_ptr<BlkIdWrapperInterface> blk_id_wrapper,
std::shared_ptr<brillo::Platform> platform,
std::shared_ptr<vpd::Vpd> vpd)
: ScreenBase(
/*button_count=*/kNumButtons,
/*index_=*/static_cast<int>(ButtonIndex::kDeviceDropDown),
State::DEBUG_OPTIONS,
draw_utils,
screen_controller),
log_store_manager_(log_store_manager),
state_(DropDownState::kDropdownClosed),
max_dropdown_items_(
(draw_utils_->GetFreconCanvasSize() / 2 - kBtnYStep * 2) /
kItemHeight -
1),
blk_id_wrapper_(blk_id_wrapper),
platform_(platform),
vpd_(vpd) {}
void ScreenDebugOptions::Show() {
draw_utils_->MessageBaseScreen();
int frecon_size = draw_utils_->GetFreconCanvasSize();
const auto kX = -frecon_size / 2 + kDefaultMessageWidth / 2;
const auto kY = -frecon_size / 2 + 220 + 18;
draw_utils_->ShowMessage("title_debug_options", kX, kY);
ShowButtons();
SetState(State::DEBUG_OPTIONS);
}
void ScreenDebugOptions::ShowCollapsedDeviceOptions(bool is_selected) {
const int frecon_canvas_size = draw_utils_->GetFreconCanvasSize();
constexpr int kOffsetY = -100;
constexpr int kBackgroundOffset = 145;
constexpr int kTextOffset = 80;
const int kBgX = -frecon_canvas_size / 2 + kBackgroundOffset;
const int kTextX = -frecon_canvas_size / 2 + kTextOffset;
draw_utils_->ShowDropDownClosed(kBgX, kOffsetY, kTextX, "btn_copy_logs",
"settings.png", is_selected);
}
void ScreenDebugOptions::UpdateStorageDevices(
std::unique_ptr<brillo::Udev> udev) {
storage_devices_.clear();
// Storing logs to disk is always an option.
storage_devices_.emplace_back(kDiskStorageDevice, std::nullopt);
button_count_ = 2;
std::vector<base::FilePath> device_paths;
if (!GetRemovableDevices(device_paths, std::move(udev))) {
LOG(WARNING) << "Couldn't scan for removable devices.";
return;
}
for (const auto& device : device_paths) {
// Generate a label if we can't find one for this device.
const auto label =
blk_id_wrapper_->CheckAndGetTagValue(kLabelTag, device.value());
const auto& display_label =
label.has_value()
? label.value()
: ("Removable Device " + base::ToString(storage_devices_.size()));
storage_devices_.emplace_back(display_label, device);
LOG(INFO) << "Added, device_label=" << display_label << " path=" << device;
}
button_count_ = storage_devices_.size() + 1;
}
void ScreenDebugOptions::ShowOpenDeviceOptions(int current_index) {
const int frecon_canvas_size = draw_utils_->GetFreconCanvasSize();
int offset_y = -100 + kBtnYStep;
constexpr int kBackgroundOffset = 260;
const int kBackgroundX = -frecon_canvas_size / 2 + kBackgroundOffset;
constexpr int kBackgroundWidth = 520;
constexpr int kHalfBackgroundWidth = (kBackgroundWidth / 2) - 40;
const int kTextOffsetX =
(draw_utils_->IsLocaleRightToLeft() ? -kBackgroundX : kBackgroundX) -
kHalfBackgroundWidth;
// Pick begin index such that the selected index is centered on the screen.
// If there are not enough items for a full page then start at 0.
int begin_index = 0;
int page_difference = storage_devices_.size() - max_dropdown_items_;
if (page_difference >= 0) {
begin_index =
std::clamp(current_index - max_dropdown_items_ / 2, 0, page_difference);
}
const auto items_to_display =
std::min(begin_index + max_dropdown_items_,
static_cast<int>(storage_devices_.size()));
for (int i = begin_index; i < items_to_display;
i++, offset_y += kItemHeight) {
if (current_index != i) {
draw_utils_->ShowBox(kBackgroundX, offset_y, kBackgroundWidth,
kItemHeight, kMenuDropdownFrameNavy);
draw_utils_->ShowBox(kBackgroundX, offset_y, kBackgroundWidth - 2,
kItemHeight - 2, kMenuDropdownBackgroundBlack);
draw_utils_->ShowText(storage_devices_[i].first, kTextOffsetX, offset_y,
"dropdown_grey");
} else {
draw_utils_->ShowBox(kBackgroundX, offset_y, kBackgroundWidth,
kItemHeight, kMenuBlue);
draw_utils_->ShowText(storage_devices_[i].first, kTextOffsetX, offset_y,
"black");
}
}
}
void ScreenDebugOptions::ShowButtons() {
int default_width = draw_utils_->GetDefaultButtonWidth();
constexpr int kYOffset = -100;
const int kYStep = kButtonHeight + kButtonMargin;
switch (state_) {
case DropDownState::kDropdownClosed: {
const auto button_index =
static_cast<ButtonIndex>(std::min(kNumButtons - 1, index_));
draw_utils_->ShowLanguageMenu(button_index ==
ButtonIndex::kLanguageDropdown);
ShowCollapsedDeviceOptions(button_index == ButtonIndex::kDeviceDropDown);
for (const auto& [index, button_token] : kButtonLabels) {
draw_utils_->ShowButton(
button_token, kYOffset + ((static_cast<int>(index) - 1) * kYStep),
button_index == index, default_width, false);
}
draw_utils_->ShowPowerButton(button_index == ButtonIndex::kPower);
break;
}
case DropDownState::kDropdownOpen: {
const auto device_index =
std::min(static_cast<int>(storage_devices_.size()), index_);
draw_utils_->ShowLanguageMenu(false);
ShowCollapsedDeviceOptions(false);
ShowOpenDeviceOptions(device_index);
const auto dropdown_size = std::min(
max_dropdown_items_, static_cast<int>(storage_devices_.size()));
// Place the back button same place as before, or lower (if the list is
// very long).
const auto back_button_y_offset =
kYOffset +
std::max(((static_cast<int>(ButtonIndex::kBack) - 1) * kYStep),
((dropdown_size + 1) * kYStep));
draw_utils_->ShowButton(kBackImage, back_button_y_offset,
(device_index == storage_devices_.size()),
default_width, false);
break;
}
}
}
void ScreenDebugOptions::HandleButtonSelection() {
const auto button_index =
static_cast<ButtonIndex>(std::min(kNumButtons - 1, index_));
switch (button_index) {
case ButtonIndex::kLanguageDropdown:
screen_controller_->SwitchLocale(this);
break;
case ButtonIndex::kDeviceDropDown:
state_ = DropDownState::kDropdownOpen;
index_ = 0;
UpdateStorageDevices();
Show();
break;
case ButtonIndex::kEraseLogs:
if (!ClearLogStoreKey(vpd_)) {
LOG(WARNING) << "Failed to clear log store key from VPD.";
}
if (log_store_manager_) {
log_store_manager_->ClearLogs();
} else {
LOG(ERROR) << "Log store invalid, cannot clear logs.";
}
break;
case ButtonIndex::kMessagLog:
screen_controller_->OnForward(this);
break;
case ButtonIndex::kBack:
screen_controller_->OnBackward(this);
break;
case ButtonIndex::kPower:
TriggerShutdown();
break;
default:
LOG(FATAL) << "Index " << index_ << " is not valid.";
}
}
void ScreenDebugOptions::HandleDeviceSelection() {
const auto device_index =
std::min(static_cast<int>(storage_devices_.size()), index_);
Reset();
Show();
if (device_index == storage_devices_.size()) {
return;
}
const auto& [label, path] = storage_devices_.at(device_index);
bool result = false;
if (log_store_manager_) {
if (label == kDiskStorageDevice) {
result = log_store_manager_->SaveLogs(
LogStoreManagerInterface::LogDirection::Disk);
} else if (path) {
result = SaveLogsToDevice(path.value());
} else {
LOG(ERROR) << "No path specified for removable device with label="
<< label;
}
} else {
LOG(ERROR) << "Log store manager not available.";
}
draw_utils_->ShowText(
(result ? "Logs successfully saved to " : "Failed to save logs to ") +
label,
-draw_utils_->GetFreconCanvasSize() / 2 + 360, -100, "grey");
}
void ScreenDebugOptions::OnKeyPress(int key_changed) {
bool enter = false;
UpdateButtonsIndex(key_changed, &enter);
if (enter) {
switch (state_) {
case DropDownState::kDropdownClosed:
HandleButtonSelection();
break;
case DropDownState::kDropdownOpen:
HandleDeviceSelection();
break;
}
} else if (state_ == DropDownState::kDropdownOpen && key_changed == KEY_ESC) {
Reset();
Show();
} else {
ShowButtons();
}
}
bool ScreenDebugOptions::MountRemovableDevice(
base::ScopedTempDir& temp_mount_folder,
const base::FilePath& device_path) const {
if (!base::PathExists(device_path)) {
LOG(ERROR) << "Device path does not exist=" << device_path.value();
return false;
}
if (!temp_mount_folder.CreateUniqueTempDir() ||
!temp_mount_folder.IsValid()) {
LOG(ERROR) << "Failed to create temp dir.";
return false;
}
const auto filesystem = blk_id_wrapper_->CheckAndGetTagValue(
kFilesystemTypeTag, device_path.value());
if (!filesystem) {
LOG(ERROR) << "Couldn't determine filesystem for device at="
<< device_path.value();
return false;
}
if (platform_->Mount(device_path.value(), temp_mount_folder.GetPath().value(),
filesystem.value(), MS_NOEXEC | MS_NOSUID) != 0) {
PLOG(ERROR) << "Failed to mount device=" << device_path.value()
<< " at temp path=" << temp_mount_folder.GetPath()
<< " filesystem=" << filesystem.value();
return false;
}
return true;
}
bool ScreenDebugOptions::SaveLogsToDevice(base::FilePath device_path) const {
base::ScopedTempDir temp_mount_folder;
if (!MountRemovableDevice(temp_mount_folder, device_path)) {
return false;
}
const auto save_logs_result = log_store_manager_->SaveLogs(
LogStoreManagerInterface::LogDirection::RemovableDevice,
temp_mount_folder.GetPath().Append(kArchiveFileName));
if (!platform_->Unmount(temp_mount_folder.GetPath(), true, nullptr)) {
PLOG(ERROR) << "Failed to umount=" << temp_mount_folder.GetPath();
}
return save_logs_result;
}
void ScreenDebugOptions::Reset() {
state_ = DropDownState::kDropdownClosed;
index_ = static_cast<int>(ButtonIndex::kDeviceDropDown);
button_count_ = kNumButtons;
}
ScreenType ScreenDebugOptions::GetType() {
return ScreenType::kDebugOptionsScreen;
}
std::string ScreenDebugOptions::GetName() {
return "ScreenDebugOptions";
}
bool ScreenDebugOptions::MoveForward(brillo::ErrorPtr* error) {
index_ = static_cast<int>(ButtonIndex::kMessagLog);
OnKeyPress(KEY_ENTER);
return true;
}
bool ScreenDebugOptions::MoveBackward(brillo::ErrorPtr* error) {
index_ = static_cast<int>(ButtonIndex::kBack);
OnKeyPress(KEY_ENTER);
return true;
}
} // namespace minios