blob: 1b1b988f3100d937303244398a1f2f98706f9393 [file] [log] [blame]
// Copyright 2020 The Chromium OS 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 "init/clobber_ui.h"
#include <inttypes.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <string>
#include <utility>
#include <base/strings/stringprintf.h>
namespace {
bool MakeTTYRaw(const base::File& tty) {
struct termios terminal_properties;
if (tcgetattr(tty.GetPlatformFile(), &terminal_properties) != 0) {
PLOG(WARNING) << "Getting properties of output TTY failed";
return false;
}
cfmakeraw(&terminal_properties);
if (tcsetattr(tty.GetPlatformFile(), TCSANOW, &terminal_properties) != 0) {
PLOG(WARNING) << "Setting properties of output TTY failed";
return false;
}
return true;
}
bool GetTerminalWidth(const base::File& terminal, int* width_out) {
if (!width_out) {
LOG(ERROR) << "width_out cannot be NULL";
return false;
}
struct winsize window_size;
int ret = ioctl(terminal.GetPlatformFile(), TIOCGWINSZ, &window_size);
if (ret) {
PLOG(ERROR) << "TIOCGWINSZ ioctl failed";
return false;
}
*width_out = window_size.ws_col;
return true;
}
// If |terminal_width| is non-zero, we will display a progress bar based
// on that width value. Otherwise, we will not display a progress bar.
std::string BuildUiString(int terminal_width,
const base::TimeDelta& elapsed,
double progress) {
std::string elapsed_time =
base::StringPrintf("%d:%02d:%02" PRIi64, elapsed.InHours(),
elapsed.InMinutes() % 60, elapsed.InSeconds() % 60);
std::string percent_done = base::StringPrintf("%3.0f%%", progress * 100);
// Determine how much space we would have for a progress bar in our terminal.
int progress_bar_width = 0;
if (terminal_width > 0) {
// Subtract 2 for padding spaces and 2 for bounding brackets.
progress_bar_width =
terminal_width - elapsed_time.length() - percent_done.length() - 2 - 2;
}
// Only show a progress bar if we have space.
if (progress_bar_width > 0) {
std::string progress_bar(progress_bar_width * progress, '=');
if (progress_bar.length() < progress_bar_width) {
// If we haven't filled up the width, add a '>' to the end of the
// progress bar and pad with zeros to fill the width.
progress_bar += ">";
progress_bar +=
std::string(progress_bar_width - progress_bar.length(), ' ');
}
return elapsed_time + " [" + progress_bar + "] " + percent_done;
} else {
return elapsed_time + " " + percent_done;
}
}
} // namespace
ClobberUi::ClobberUi(base::File&& terminal)
: terminal_(std::move(terminal)), mode_(kIdle) {
if (!MakeTTYRaw(terminal_)) {
LOG(ERROR) << "Failed to set terminal to raw mode.";
}
}
bool ClobberUi::StartWipeUi(int64_t bytes_to_write) {
if (!wipe_ui_thread_.is_null()) {
LOG(ERROR) << "UI is already running.";
return false;
}
mode_ = kWipeUi;
base::AutoLock auto_lock(lock_);
state_.running = true;
state_.total_bytes_written = 0;
state_.bytes_to_write = bytes_to_write;
if (!base::PlatformThread::Create(0, this, &wipe_ui_thread_)) {
LOG(ERROR) << "Failed to create wipe UI thread.";
return false;
}
return true;
}
bool ClobberUi::UpdateWipeProgress(int64_t total_bytes_written) {
base::AutoLock auto_lock(lock_);
if (wipe_ui_thread_.is_null() || mode_ != kWipeUi || !state_.running) {
LOG(ERROR) << "Cannot update progress, wipe UI is not running.";
return false;
}
state_.total_bytes_written = total_bytes_written;
return true;
}
bool ClobberUi::StopWipeUi() {
if (wipe_ui_thread_.is_null()) {
LOG(ERROR) << "Failed to stop wipe UI, thread is not running.";
return false;
}
if (mode_ != kWipeUi) {
LOG(ERROR) << "Failed to stop wipe UI, current UI is not a wipe UI.";
return false;
}
{
base::AutoLock auto_lock(lock_);
if (!state_.running) {
LOG(ERROR) << "Failed to stop wipe UI, wipe UI is already being stopped.";
return false;
}
state_.running = false;
}
base::PlatformThread::Join(wipe_ui_thread_);
wipe_ui_thread_ = base::PlatformThreadHandle();
mode_ = kIdle;
return true;
}
bool ClobberUi::ShowCountdownTimer(const base::TimeDelta& duration) {
if (mode_ != kIdle) {
LOG(ERROR) << "Failed to show countdown timer, UI is already in use.";
return false;
}
mode_ = kCountdownUi;
base::TimeTicks start_time = base::TimeTicks::Now();
base::TimeDelta elapsed = base::TimeTicks::Now() - start_time;
while (elapsed < duration) {
base::TimeDelta remaining = duration - elapsed;
std::string countdown =
base::StringPrintf("%2d:%02" PRIi64 "\r", remaining.InMinutes(),
remaining.InSeconds() % 60);
terminal_.WriteAtCurrentPos(countdown.c_str(), countdown.size());
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
elapsed = base::TimeTicks::Now() - start_time;
}
mode_ = kIdle;
return true;
}
// static
std::string ClobberUi::BuildUiStringForTest(int terminal_width,
const base::TimeDelta& elapsed,
double progress) {
return BuildUiString(terminal_width, elapsed, progress);
}
void ClobberUi::ThreadMain() {
base::TimeTicks start_time = base::TimeTicks::Now();
base::AutoLock auto_lock(lock_);
bool show_progress_bar = true;
while (state_.running) {
base::TimeDelta elapsed = base::TimeTicks::Now() - start_time;
double progress =
static_cast<double>(state_.total_bytes_written) / state_.bytes_to_write;
int terminal_width = 0;
if (show_progress_bar && !GetTerminalWidth(terminal_, &terminal_width)) {
LOG(WARNING)
<< "Getting terminal width failed. No progress bar will be shown.";
show_progress_bar = false;
}
std::string output = BuildUiString(terminal_width, elapsed, progress);
terminal_.WriteAtCurrentPos("\r", 1);
terminal_.WriteAtCurrentPos(output.c_str(), output.size());
{
base::AutoUnlock auto_unlock(lock_);
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(100));
}
}
}