Commit 98d99292 authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

Better status reporting for crostini terminal startup

Created a new class CrostiniStartupStatus which shows
a progress bar and spinner, and adds color and emojis.

Full status is shown when the terminal starts and
the container is not running.  Otherwise no status is
shown if the container is already running.

TBR=benwells@chromium.org

Bug: 1016680
Change-Id: I726d9d721937f74a40bba646339a3e9fd97e5a36
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1888239
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Reviewed-by: default avatarJulian Watson <juwa@google.com>
Cr-Commit-Position: refs/heads/master@{#711156}
parent 67969167
......@@ -952,6 +952,8 @@ jumbo_static_library("extensions") {
"api/settings_private/chromeos_resolve_time_zone_by_geolocation_on_off.h",
"api/settings_private/generated_time_zone_pref_base.cc",
"api/settings_private/generated_time_zone_pref_base.h",
"api/terminal/crostini_startup_status.cc",
"api/terminal/crostini_startup_status.h",
"api/terminal/terminal_extension_helper.cc",
"api/terminal/terminal_extension_helper.h",
"api/terminal/terminal_private_api.cc",
......@@ -975,6 +977,7 @@ jumbo_static_library("extensions") {
"//ash",
"//ash/keyboard/ui:resources_grit_grit",
"//ash/public/cpp",
"//chrome/browser/chromeos/crostini:crostini_installer_types_mojom",
"//chrome/browser/resources/chromeos/camera:chrome_camera_app",
"//chromeos",
"//chromeos/attestation",
......
// Copyright 2019 The Chromium 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 "chrome/browser/extensions/api/terminal/crostini_startup_status.h"
#include <algorithm>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/time/time.h"
#include "chromeos/dbus/util/version_loader.h"
#include "components/version_info/version_info.h"
namespace extensions {
namespace {
const char kCursorHide[] = "\x1b[?25l";
const char kCursorShow[] = "\x1b[?25h";
const char kColor0Normal[] = "\x1b[0m"; // Default.
const char kColor1Red[] = "\x1b[31m";
const char kColor2Green[] = "\x1b[32m";
const char kColor4Blue[] = "\x1b[34m";
const char kColor5Purple[] = "\x1b[35m";
const char kProgressStart[] = "\x1b[7m"; // Invert color.
const char kProgressEnd[] = "\x1b[27m"; // Revert color.
const char kSpinner[] = "|/-\\";
const int kTimestampLength = 25;
const int kMaxProgress = 9;
const base::NoDestructor<std::vector<std::string>> kSuccessEmoji(
{"😀", "😉", "🤩", "🤪", "😎", "🥳", "👍"});
const base::NoDestructor<std::vector<std::string>> kErrorEmoji({"🤕", "😠",
"😧", "😢", "😞"});
} // namespace
CrostiniStartupStatus::CrostiniStartupStatus(
base::RepeatingCallback<void(const std::string&)> print,
bool verbose,
base::OnceClosure callback)
: print_(std::move(print)),
verbose_(verbose),
callback_(std::move(callback)) {
Print(kCursorHide);
if (verbose_) {
PrintWithTimestamp("Chrome OS " + version_info::GetVersionNumber() + " " +
chromeos::version_loader::GetVersion(
chromeos::version_loader::VERSION_FULL) +
"\r\n");
}
}
CrostiniStartupStatus::~CrostiniStartupStatus() = default;
void CrostiniStartupStatus::OnCrostiniRestarted(
crostini::CrostiniResult result) {
if (result != crostini::CrostiniResult::SUCCESS) {
LOG(ERROR) << "Error starting crostini for terminal: "
<< static_cast<int>(result);
PrintWithTimestamp(base::StringPrintf(
"Error starting penguin container: %d %s\r\n", result,
(*kErrorEmoji)[rand() % kErrorEmoji->size()].c_str()));
} else {
if (verbose_) {
PrintWithTimestamp(base::StringPrintf(
"Ready %s\r\n",
(*kSuccessEmoji)[rand() % kSuccessEmoji->size()].c_str()));
}
Print(kCursorShow);
}
std::move(callback_).Run();
delete this;
}
void CrostiniStartupStatus::ShowStatusLineAtInterval() {
++spinner_index_;
PrintStatusLine();
base::PostDelayedTask(
FROM_HERE,
base::BindOnce(&CrostiniStartupStatus::ShowStatusLineAtInterval,
weak_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(300));
}
void CrostiniStartupStatus::OnStageStarted(InstallerState stage) {
stage_ = stage;
progress_index_++;
if (!verbose_) {
return;
}
static base::NoDestructor<base::flat_map<InstallerState, std::string>>
kStartStrings({
{InstallerState::kStart, "Starting... 🤔"},
{InstallerState::kInstallImageLoader,
"Checking cros-termina component..."},
{InstallerState::kStartConcierge, "Starting VM controller..."},
{InstallerState::kCreateDiskImage, "Creating termina VM image..."},
{InstallerState::kStartTerminaVm, "Starting termina VM..."},
{InstallerState::kCreateContainer, "Creating penguin container..."},
{InstallerState::kSetupContainer,
"Checking penguin container setup..."},
{InstallerState::kStartContainer, "Starting penguin container..."},
{InstallerState::kFetchSshKeys,
"Fetching penguin container ssh keys..."},
{InstallerState::kMountContainer,
"Mounting penguin container sshfs..."},
});
const std::string& start_string = (*kStartStrings)[stage];
cursor_position_ = kTimestampLength + start_string.length();
PrintWithTimestamp(start_string + "\r\n");
PrintStatusLine();
}
void CrostiniStartupStatus::OnComponentLoaded(crostini::CrostiniResult result) {
PrintCrostiniResult(result);
}
void CrostiniStartupStatus::OnConciergeStarted(bool success) {
PrintSuccess(success);
}
void CrostiniStartupStatus::OnDiskImageCreated(
bool success,
vm_tools::concierge::DiskImageStatus status,
int64_t disk_size_available) {
PrintSuccess(success);
}
void CrostiniStartupStatus::OnVmStarted(bool success) {
PrintSuccess(success);
}
void CrostiniStartupStatus::OnContainerDownloading(int32_t download_percent) {
if (download_percent % 8 == 0) {
PrintResult(".");
}
}
void CrostiniStartupStatus::OnContainerCreated(
crostini::CrostiniResult result) {
PrintCrostiniResult(result);
}
void CrostiniStartupStatus::OnContainerSetup(bool success) {
PrintSuccess(success);
}
void CrostiniStartupStatus::OnContainerStarted(
crostini::CrostiniResult result) {
PrintCrostiniResult(result);
}
void CrostiniStartupStatus::OnSshKeysFetched(bool success) {
PrintSuccess(success);
}
void CrostiniStartupStatus::OnContainerMounted(bool success) {
PrintSuccess(success);
}
void CrostiniStartupStatus::PrintStatusLine() {
std::string progress(progress_index_, ' ');
std::string dots(std::max(kMaxProgress - progress_index_, 0), '.');
Print(base::StringPrintf("[%s%s%s%s%s%s] %s%c%s\r", kProgressStart,
progress.c_str(), kProgressEnd, kColor5Purple,
dots.c_str(), kColor0Normal, kColor4Blue,
kSpinner[spinner_index_ & 0x3], kColor0Normal));
}
void CrostiniStartupStatus::Print(const std::string& output) {
print_.Run(output);
}
void CrostiniStartupStatus::PrintWithTimestamp(const std::string& output) {
base::Time::Exploded exploded;
base::Time::Now().LocalExplode(&exploded);
Print(base::StringPrintf("%04d-%02d-%02d %02d:%02d:%02d.%03d %s",
exploded.year, exploded.month, exploded.day_of_month,
exploded.hour, exploded.minute, exploded.second,
exploded.millisecond, output.c_str()));
}
void CrostiniStartupStatus::PrintResult(const std::string& output) {
if (!verbose_) {
return;
}
std::string cursor_move = "\x1b[A"; // cursor up.
for (int i = 0; i < cursor_position_; ++i) {
cursor_move += "\x1b[C"; // cursor forward.
}
Print(cursor_move + output + "\r\n");
cursor_position_ += output.length();
PrintStatusLine();
}
void CrostiniStartupStatus::PrintCrostiniResult(
crostini::CrostiniResult result) {
if (result == crostini::CrostiniResult::SUCCESS) {
PrintSuccess(true);
} else {
PrintResult(base::StringPrintf("%serror=%d%s ❌", kColor1Red, result,
kColor0Normal));
}
}
void CrostiniStartupStatus::PrintSuccess(bool success) {
if (success) {
PrintResult(
base::StringPrintf("%sdone%s ✔️", kColor2Green, kColor0Normal));
} else {
PrintResult(base::StringPrintf("%serror%s ❌", kColor1Red, kColor0Normal));
}
}
} // namespace extensions
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_EXTENSIONS_API_TERMINAL_CROSTINI_STARTUP_STATUS_H_
#define CHROME_BROWSER_EXTENSIONS_API_TERMINAL_CROSTINI_STARTUP_STATUS_H_
#include <string>
#include "base/bind.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/chromeos/crostini/crostini_installer_types.mojom.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_simple_types.h"
using crostini::mojom::InstallerState;
namespace extensions {
// Displays startup status to the crostini terminal.
class CrostiniStartupStatus
: public crostini::CrostiniManager::RestartObserver {
public:
CrostiniStartupStatus(base::RepeatingCallback<void(const std::string&)> print,
bool verbose,
base::OnceClosure callback);
~CrostiniStartupStatus() override;
// Updates the status line every 300ms.
void ShowStatusLineAtInterval();
// Deletes this object when called.
void OnCrostiniRestarted(crostini::CrostiniResult result);
private:
FRIEND_TEST_ALL_PREFIXES(CrostiniStartupStatusTest, TestNotVerbose);
FRIEND_TEST_ALL_PREFIXES(CrostiniStartupStatusTest, TestVerbose);
// crostini::CrostiniManager::RestartObserver
void OnStageStarted(InstallerState stage) override;
void OnComponentLoaded(crostini::CrostiniResult result) override;
void OnConciergeStarted(bool success) override;
void OnDiskImageCreated(bool success,
vm_tools::concierge::DiskImageStatus status,
int64_t disk_size_available) override;
void OnVmStarted(bool success) override;
void OnContainerDownloading(int32_t download_percent) override;
void OnContainerCreated(crostini::CrostiniResult result) override;
void OnContainerSetup(bool success) override;
void OnContainerStarted(crostini::CrostiniResult result) override;
void OnSshKeysFetched(bool success) override;
void OnContainerMounted(bool success) override;
void PrintStatusLine();
void Print(const std::string& output);
void PrintWithTimestamp(const std::string& output);
// Moves cursor up and to the right to previous line before status line before
// printing output.
void PrintResult(const std::string& output);
void PrintCrostiniResult(crostini::CrostiniResult result);
void PrintSuccess(bool success);
base::RepeatingCallback<void(const std::string& output)> print_;
const bool verbose_;
base::OnceClosure callback_;
int spinner_index_ = 0;
int progress_index_ = 0;
// Position of cursor on line above status line.
int cursor_position_ = 0;
InstallerState stage_ = InstallerState::kStart;
base::WeakPtrFactory<CrostiniStartupStatus> weak_factory_{this};
};
} // namespace extensions
#endif // CHROME_BROWSER_EXTENSIONS_API_TERMINAL_CROSTINI_STARTUP_STATUS_H_
// Copyright 2019 The Chromium 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 "chrome/browser/extensions/api/terminal/crostini_startup_status.h"
#include <memory>
#include <vector>
#include "base/bind.h"
#include "chrome/browser/chromeos/crostini/crostini_simple_types.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace extensions {
class CrostiniStartupStatusTest : public testing::Test {
protected:
void Print(const std::string& output) {
output_.emplace_back(std::move(output));
}
void Done() { done_ = true; }
CrostiniStartupStatus* NewStartupStatus(bool verbose) {
return new CrostiniStartupStatus(
base::BindRepeating(&CrostiniStartupStatusTest::Print,
base::Unretained(this)),
verbose,
base::BindOnce(&CrostiniStartupStatusTest::Done,
base::Unretained(this)));
}
void SetUp() override {}
std::vector<std::string> output_;
bool done_ = false;
};
TEST_F(CrostiniStartupStatusTest, TestNotVerbose) {
auto* startup_status = NewStartupStatus(false);
startup_status->OnStageStarted(InstallerState::kStart);
startup_status->OnStageStarted(InstallerState::kInstallImageLoader);
startup_status->OnComponentLoaded(crostini::CrostiniResult::SUCCESS);
startup_status->OnCrostiniRestarted(crostini::CrostiniResult::SUCCESS);
EXPECT_TRUE(done_);
// Hides cursor, shows cursor.
EXPECT_EQ(output_.size(), 2u);
EXPECT_EQ(output_[0], "\x1b[?25l");
EXPECT_EQ(output_[1], "\x1b[?25h");
}
TEST_F(CrostiniStartupStatusTest, TestVerbose) {
auto* startup_status = NewStartupStatus(true);
startup_status->OnStageStarted(InstallerState::kStart);
startup_status->OnStageStarted(InstallerState::kInstallImageLoader);
startup_status->OnComponentLoaded(crostini::CrostiniResult::SUCCESS);
startup_status->OnCrostiniRestarted(crostini::CrostiniResult::SUCCESS);
EXPECT_TRUE(done_);
// Hides cursor, version, start, status, component, status, done, status,
// ready, shows cursor.
EXPECT_EQ(output_.size(), 10u);
EXPECT_EQ(output_[0], "\x1b[?25l");
EXPECT_EQ(output_[1].find("Chrome OS "), 24u);
EXPECT_EQ(output_[2].substr(24), "Starting... 🤔\r\n");
EXPECT_EQ(output_[3],
"[\x1b[7m \x1b[27m\x1b[35m........\x1b[0m] \x1b[34m|\x1b[0m\r");
EXPECT_EQ(output_[4].substr(24), "Checking cros-termina component...\r\n");
EXPECT_EQ(output_[5],
"[\x1b[7m \x1b[27m\x1b[35m.......\x1b[0m] \x1b[34m|\x1b[0m\r");
std::string expected = "\x1b[A";
for (int i = 0; i < 59; ++i)
expected += "\x1b[C";
expected += "\x1b[32mdone\x1b[0m \xE2\x9C\x94\xEF\xb8\x8F\r\n";
EXPECT_EQ(output_[6], expected);
EXPECT_EQ(output_[7],
"[\x1b[7m \x1b[27m\x1b[35m.......\x1b[0m] \x1b[34m|\x1b[0m\r");
EXPECT_EQ(output_[8].find("Ready"), 24u);
EXPECT_EQ(output_[9], "\x1b[?25h");
}
} // namespace extensions
......@@ -12,23 +12,18 @@
#include "base/bind.h"
#include "base/command_line.h"
#include "base/json/json_writer.h"
#include "base/strings/stringprintf.h"
#include "base/memory/scoped_refptr.h"
#include "base/system/sys_info.h"
#include "base/task/post_task.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_simple_types.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/extensions/api/terminal/crostini_startup_status.h"
#include "chrome/browser/extensions/api/terminal/terminal_extension_helper.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/api/terminal_private.h"
#include "chromeos/dbus/util/version_loader.h"
#include "chromeos/process_proxy/process_proxy_registry.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
......@@ -118,112 +113,6 @@ int GetTabOrWindowSessionId(content::BrowserContext* browser_context,
return window ? window->session_id().id() : -1;
}
class CrostiniRestartObserver
: public crostini::CrostiniManager::RestartObserver {
public:
CrostiniRestartObserver(
base::RepeatingCallback<void(const std::string&)> print,
base::OnceClosure callback)
: print_(std::move(print)), callback_(std::move(callback)) {}
void OnCrostiniRestarted(crostini::CrostiniResult result) {
if (result != crostini::CrostiniResult::SUCCESS) {
LOG(ERROR) << "Error starting crostini for terminal: "
<< static_cast<int>(result);
PrintWithTimestamp(base::StringPrintf(
"Error starting penguin container: %d\r\n", result));
} else {
PrintWithTimestamp("Ready\r\n");
}
std::move(callback_).Run();
delete this;
}
private:
// crostini::CrostiniManager::RestartObserver
void OnStageStarted(InstallerState stage) override {
switch (stage) {
case InstallerState::kStart:
PrintWithTimestamp("Chrome OS " + version_info::GetVersionNumber() +
" " +
chromeos::version_loader::GetVersion(
chromeos::version_loader::VERSION_FULL) +
"\r\n");
PrintWithTimestamp("Starting terminal...\r\n");
break;
case InstallerState::kInstallImageLoader:
PrintWithTimestamp("Checking cros-termina component... ");
break;
case InstallerState::kStartConcierge:
PrintWithTimestamp("Starting VM controller... ");
break;
case InstallerState::kCreateDiskImage:
PrintWithTimestamp("Creating termina VM image... ");
break;
case InstallerState::kStartTerminaVm:
PrintWithTimestamp("Starting termina VM... ");
break;
case InstallerState::kCreateContainer:
PrintWithTimestamp("Creating penguin container... ");
break;
case InstallerState::kSetupContainer:
PrintWithTimestamp("Checking penguin container setup... ");
break;
case InstallerState::kStartContainer:
PrintWithTimestamp("Starting penguin container... ");
break;
case InstallerState::kFetchSshKeys:
PrintWithTimestamp("Fetching penguin container ssh keys... ");
break;
case InstallerState::kMountContainer:
PrintWithTimestamp("Mounting penguin container sshfs... ");
break;
}
}
void OnComponentLoaded(crostini::CrostiniResult result) override {
PrintCrostiniResult(result);
}
void OnConciergeStarted(bool success) override { PrintSuccess(success); }
void OnDiskImageCreated(bool success,
vm_tools::concierge::DiskImageStatus status,
int64_t disk_size_available) override {
PrintSuccess(success);
}
void OnVmStarted(bool success) override { PrintSuccess(success); }
void OnContainerDownloading(int32_t download_percent) override { Print("."); }
void OnContainerCreated(crostini::CrostiniResult result) override {
PrintCrostiniResult(result);
}
void OnContainerSetup(bool success) override { PrintSuccess(success); }
void OnContainerStarted(crostini::CrostiniResult result) override {
PrintCrostiniResult(result);
}
void OnSshKeysFetched(bool success) override { PrintSuccess(success); }
void OnContainerMounted(bool success) override { PrintSuccess(success); }
void Print(const std::string& output) { print_.Run(output); }
void PrintWithTimestamp(const std::string& output) {
base::Time::Exploded exploded;
base::Time::Now().LocalExplode(&exploded);
Print(base::StringPrintf(
"%04d-%02d-%02d %02d:%02d:%02d.%03d %s", exploded.year, exploded.month,
exploded.day_of_month, exploded.hour, exploded.minute, exploded.second,
exploded.millisecond, output.c_str()));
}
void Println(const std::string& output) { Print(output + "\r\n"); }
void PrintCrostiniResult(crostini::CrostiniResult result) {
if (result == crostini::CrostiniResult::SUCCESS) {
Println("done");
} else {
Println(base::StringPrintf("error=%d", result));
}
}
void PrintSuccess(bool success) { Println(success ? "done" : "error"); }
base::RepeatingCallback<void(const std::string& output)> print_;
base::OnceClosure callback_;
};
} // namespace
namespace extensions {
......@@ -295,19 +184,24 @@ TerminalPrivateOpenTerminalProcessFunction::Run() {
auto open_process =
base::BindOnce(&TerminalPrivateOpenTerminalProcessFunction::OpenProcess,
this, user_id_hash, tab_id, vmshell_cmd.argv());
auto* observer = new CrostiniRestartObserver(
auto* mgr = crostini::CrostiniManager::GetForProfile(
Profile::FromBrowserContext(browser_context()));
bool verbose =
!mgr->GetContainerInfo(crostini::kCrostiniDefaultVmName,
crostini::kCrostiniDefaultContainerName)
.has_value();
auto* observer = new CrostiniStartupStatus(
base::BindRepeating(&NotifyProcessOutput, browser_context(), tab_id,
startup_id,
api::terminal_private::ToString(
api::terminal_private::OUTPUT_TYPE_STDOUT)),
std::move(open_process));
crostini::CrostiniManager::GetForProfile(
Profile::FromBrowserContext(browser_context()))
->RestartCrostini(
vm_name, container_name,
base::BindOnce(&CrostiniRestartObserver::OnCrostiniRestarted,
base::Unretained(observer)),
observer);
verbose, std::move(open_process));
observer->ShowStatusLineAtInterval();
mgr->RestartCrostini(
vm_name, container_name,
base::BindOnce(&CrostiniStartupStatus::OnCrostiniRestarted,
base::Unretained(observer)),
observer);
} else {
// command=[unrecognized].
return RespondNow(Error("Invalid process name: " + params->process_name));
......
......@@ -4088,6 +4088,7 @@ test("unit_tests") {
sources += [
"../browser/component_updater/cros_component_installer_chromeos_unittest.cc",
"../browser/component_updater/metadata_table_chromeos_unittest.cc",
"../browser/extensions/api/terminal/crostini_startup_status_unittest.cc",
"../browser/google/google_brand_code_map_chromeos_unittest.cc",
"../browser/media/webrtc/desktop_media_list_ash_unittest.cc",
"../browser/metrics/perf/heap_collector_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment