| // 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 |