Commit 10c82a81 authored by Fergus Dall's avatar Fergus Dall Committed by Commit Bot

crostini: Add precondition checks for crostini upgrade

Currently we will check for an active network connection, a charging
battery, and at least 1 GiB of disk space.

Bug: 930901
Change-Id: Id742be21a985232ab8dc6f55e11e109151c13833
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1959242Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Reviewed-by: default avatarHector Carmona <hcarmona@chromium.org>
Reviewed-by: default avatarSam McNally <sammc@chromium.org>
Commit-Queue: Fergus Dall <sidereal@google.com>
Cr-Commit-Position: refs/heads/master@{#728777}
parent 3aae1a80
......@@ -3607,6 +3607,18 @@
<message name="IDS_CROSTINI_UPGRADER_BACKUP_SUCCEEDED_MESSAGE" desc="Text shown by the Crostini upgrader when the container backup completed successfully, prior to the main upgrade.">
Linux apps and files have been successfully backed up. Upgrade will begin shortly.
</message>
<message name="IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_TITLE" desc="Title of the Crostini upgrader when the upgrade prechecks failed.">
Error starting upgrade
</message>
<message name="IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_NETWORK" desc="Text shown by the Crostini upgrader when the upgrade did not start because there was no network connection.">
A network connection is required to upgrade Linux. Please connect to the internet and try again.
</message>
<message name="IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_POWER" desc="Text shown by the Crostini upgrader when the upgrade did not start because there wasn't enough power.">
Upgrading Linux can drain your battery significantly. Please connect your device to a charger and try again.
</message>
<message name="IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_SPACE" desc="Text shown by the Crostini upgrader when the upgrade did not start because there wasn't enough free disk space.">
At least <ph name="REQUIRED_SPACE">$1<ex>1GB</ex></ph> of free disk space is required to upgrade Linux. Please free some space on your device and try again.
</message>
<message name="IDS_CROSTINI_UPGRADER_UPGRADING_TITLE" desc="Title of the Crostini upgrader when the upgrade is in progress.">
Upgrading Linux
</message>
......
......@@ -76,6 +76,7 @@ source_set("chromeos") {
"//chrome/browser/resource_coordinator:tab_metrics_event_proto",
"//chrome/browser/ssl:proto",
"//chrome/browser/ui/webui/bluetooth_internals:mojo_bindings",
"//chrome/browser/ui/webui/chromeos/crostini_upgrader:mojo_bindings",
"//chrome/browser/web_applications",
"//chrome/browser/web_applications:web_applications_on_extensions",
"//chrome/browser/web_applications/components",
......
......@@ -4,6 +4,8 @@
#include "chrome/browser/chromeos/crostini/crostini_upgrader.h"
#include "base/barrier_closure.h"
#include "base/system/sys_info.h"
#include "base/task/post_task.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_manager_factory.h"
......@@ -17,6 +19,7 @@
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_instance.h"
#include "services/network/public/cpp/network_connection_tracker.h"
namespace crostini {
......@@ -58,7 +61,7 @@ CrostiniUpgrader* CrostiniUpgrader::GetForProfile(Profile* profile) {
}
CrostiniUpgrader::CrostiniUpgrader(Profile* profile)
: profile_(profile), container_id_("", "") {
: profile_(profile), container_id_("", ""), pmc_observer_(this) {
CrostiniManager::GetForProfile(profile_)->AddUpgradeContainerProgressObserver(
this);
}
......@@ -100,6 +103,72 @@ void CrostiniUpgrader::OnBackup(CrostiniResult result) {
}
}
void CrostiniUpgrader::StartPrechecks() {
auto* pmc = chromeos::PowerManagerClient::Get();
if (pmc_observer_.IsObserving(pmc)) {
// This could happen if two StartPrechecks were run at the same time. If it
// does, drop the second call.
return;
}
prechecks_callback_ =
base::BarrierClosure(2, /* Number of asynchronous prechecks to wait for */
base::BindOnce(&CrostiniUpgrader::DoPrechecks,
weak_ptr_factory_.GetWeakPtr()));
pmc_observer_.Add(pmc);
pmc->RequestStatusUpdate();
base::PostTaskAndReplyWithResult(
FROM_HERE, {base::ThreadPool(), base::MayBlock()},
base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
base::FilePath(crostini::kHomeDirectory)),
base::BindOnce(&CrostiniUpgrader::OnAvailableDiskSpace,
weak_ptr_factory_.GetWeakPtr()));
}
void CrostiniUpgrader::PowerChanged(
const power_manager::PowerSupplyProperties& proto) {
// A battery can be FULL, CHARGING, DISCHARGING, or NOT_PRESENT. If we're on a
// system with no battery, we can assume stable power from the fact that we
// are running at all. Otherwise we want the battery to be full or charging. A
// less conservative check is possible, but we can expect users to have access
// to a charger.
power_status_good_ = proto.battery_state() !=
power_manager::PowerSupplyProperties::DISCHARGING;
auto* pmc = chromeos::PowerManagerClient::Get();
pmc_observer_.Remove(pmc);
prechecks_callback_.Run();
}
void CrostiniUpgrader::OnAvailableDiskSpace(int64_t bytes) {
free_disk_space_ = bytes;
prechecks_callback_.Run();
}
void CrostiniUpgrader::DoPrechecks() {
chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus status;
if (free_disk_space_ < kDiskRequired) {
status = chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus::
INSUFFICIENT_SPACE;
} else if (content::GetNetworkConnectionTracker()->IsOffline()) {
status = chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus::
NETWORK_FAILURE;
} else if (!power_status_good_) {
status =
chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus::LOW_POWER;
} else {
status = chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus::OK;
}
for (auto& observer : upgrader_observers_) {
observer.PrecheckStatus(status);
}
}
void CrostiniUpgrader::Upgrade(const ContainerId& container_id) {
container_id_ = container_id;
CrostiniManager::GetForProfile(profile_)->UpgradeContainer(
......
......@@ -6,8 +6,10 @@
#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_UPGRADER_H_
#include "base/callback_forward.h"
#include "base/scoped_observer.h"
#include "chrome/browser/chromeos/crostini/crostini_manager.h"
#include "chrome/browser/chromeos/crostini/crostini_upgrader_ui_delegate.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/keyed_service/core/keyed_service.h"
class Profile;
......@@ -16,6 +18,7 @@ namespace crostini {
class CrostiniUpgrader : public KeyedService,
public UpgradeContainerProgressObserver,
public chromeos::PowerManagerClient::Observer,
public CrostiniUpgraderUIDelegate {
public:
static CrostiniUpgrader* GetForProfile(Profile* profile);
......@@ -33,6 +36,7 @@ class CrostiniUpgrader : public KeyedService,
void AddObserver(CrostiniUpgraderUIObserver* observer) override;
void RemoveObserver(CrostiniUpgraderUIObserver* observer) override;
void Backup() override;
void StartPrechecks() override;
void Upgrade(const ContainerId& container_id) override;
void Cancel() override;
void CancelBeforeStart() override;
......@@ -43,18 +47,36 @@ class CrostiniUpgrader : public KeyedService,
UpgradeContainerProgressStatus status,
const std::vector<std::string>& messages) override;
// chromeos::PowerManagerClient::Observer:
void PowerChanged(const power_manager::PowerSupplyProperties& proto) override;
// Return true if internal state allows starting upgrade.
bool CanUpgrade();
// Require at least 1 GiB of free space. Experiments on an unmodified
// container suggest this is a bare minimum, anyone with a substantial amount
// of programs installed will likely require more.
static constexpr int64_t kDiskRequired = 1 << 30;
private:
void OnCancel(CrostiniResult result);
void OnBackup(CrostiniResult result);
void OnUpgrade(CrostiniResult result);
void OnAvailableDiskSpace(int64_t bytes);
void DoPrechecks();
Profile* profile_;
ContainerId container_id_;
base::ObserverList<CrostiniUpgraderUIObserver>::Unchecked upgrader_observers_;
base::RepeatingClosure prechecks_callback_;
bool power_status_good_ = false;
int64_t free_disk_space_ = -1;
ScopedObserver<chromeos::PowerManagerClient,
chromeos::PowerManagerClient::Observer>
pmc_observer_;
base::WeakPtrFactory<CrostiniUpgrader> weak_ptr_factory_{this};
};
......
......@@ -7,6 +7,7 @@
#include "base/callback_forward.h"
#include "base/strings/string16.h"
#include "chrome/browser/ui/webui/chromeos/crostini_upgrader/crostini_upgrader.mojom.h"
namespace crostini {
......@@ -17,6 +18,8 @@ class CrostiniUpgraderUIObserver {
virtual void OnBackupProgress(int percent) = 0;
virtual void OnBackupSucceeded() = 0;
virtual void OnBackupFailed() = 0;
virtual void PrecheckStatus(
chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus status) = 0;
virtual void OnUpgradeProgress(const std::vector<std::string>& messages) = 0;
virtual void OnUpgradeSucceeded() = 0;
virtual void OnUpgradeFailed() = 0;
......@@ -33,6 +36,8 @@ class CrostiniUpgraderUIDelegate {
// Back up the current container before upgrading
virtual void Backup() = 0;
virtual void StartPrechecks() = 0;
// Start the upgrade.
virtual void Upgrade(const ContainerId& container_id) = 0;
......
......@@ -22,6 +22,7 @@ const State = {
PROMPT: 'prompt',
BACKUP: 'backup',
BACKUP_SUCCEEDED: 'backupSucceeded',
PRECHECKS_FAILED: 'prechecksFailed',
UPGRADING: 'upgrading',
ERROR: 'error',
CANCELING: 'canceling',
......@@ -86,14 +87,31 @@ Polymer({
this.state_ = State.BACKUP_SUCCEEDED;
// We do a short (2 second) interstitial display of the backup success
// message before continuing the upgrade.
setTimeout(() => {
var timeout = new Promise((resolve, reject) => {
setTimeout(resolve, 2000);
});
// We also want to wait for the prechecks to finish.
var callback = new Promise((resolve, reject) => {
this.startPrechecks_(resolve, reject);
});
Promise.all([timeout, callback]).then(() => {
this.startUpgrade_();
}, 2000);
});
}),
callbackRouter.onBackupFailed.addListener(() => {
assert(this.state_ === State.BACKUP);
this.state_ = State.ERROR;
}),
callbackRouter.precheckStatus.addListener((status) => {
this.precheckStatus_ = status;
if (status ===
chromeos.crostiniUpgrader.mojom.UpgradePrecheckStatus.OK) {
this.precheckSuccessCallback_();
} else {
this.state_ = State.PRECHECKS_FAILED;
this.precheckFailureCallback_();
}
}),
callbackRouter.onUpgradeProgress.addListener((progressMessages) => {
assert(this.state_ === State.UPGRADING);
this.progressMessages_.push(...progressMessages);
......@@ -134,11 +152,17 @@ Polymer({
BrowserProxy.getInstance().handler.launch();
this.closeDialog_();
break;
case State.PRECHECKS_FAILED:
this.startPrechecks_(() => {
this.startUpgrade_();
}, () => {});
case State.PROMPT:
if (this.backupCheckboxChecked_) {
this.startBackup_();
} else {
this.startUpgrade_();
this.startPrechecks_(() => {
this.startUpgrade_();
}, () => {});
}
break;
}
......@@ -154,6 +178,7 @@ Polymer({
this.state_ = State.CANCELING;
BrowserProxy.getInstance().handler.cancel();
break;
case State.PRECHECKS_FAILED:
case State.ERROR:
case State.SUCCEEDED:
this.closeDialog_();
......@@ -174,6 +199,13 @@ Polymer({
BrowserProxy.getInstance().handler.backup();
},
/** @private */
startPrechecks_: function(success, failure) {
this.precheckSuccessCallback_ = success;
this.precheckFailureCallback_ = failure;
BrowserProxy.getInstance().handler.startPrechecks();
},
/** @private */
startUpgrade_: function() {
this.state_ = State.UPGRADING;
......@@ -203,6 +235,7 @@ Polymer({
canDoAction_: function(state) {
switch (state) {
case State.PROMPT:
case State.PRECHECKS_FAILED:
case State.SUCCEEDED:
return true;
}
......@@ -240,6 +273,9 @@ Polymer({
case State.BACKUP_SUCCEEDED:
titleId = 'backupSucceededTitle';
break;
case State.PRECHECKS_FAILED:
titleId = 'prechecksFailedTitle';
break;
case State.UPGRADING:
titleId = 'upgradingTitle';
break;
......@@ -267,6 +303,8 @@ Polymer({
switch (state) {
case State.PROMPT:
return loadTimeData.getString('upgrade');
case State.PRECHECKS_FAILED:
return loadTimeData.getString('retry');
case State.ERROR:
return loadTimeData.getString('cancel');
case State.SUCCEEDED:
......@@ -306,6 +344,22 @@ Polymer({
case State.BACKUP_SUCCEEDED:
messageId = 'backupSucceededMessage';
break;
case State.PRECHECKS_FAILED:
switch (this.precheckStatus_) {
case chromeos.crostiniUpgrader.mojom.UpgradePrecheckStatus
.NETWORK_FAILURE:
messageId = 'precheckNoNetwork';
break;
case chromeos.crostiniUpgrader.mojom.UpgradePrecheckStatus.LOW_POWER:
messageId = 'precheckNoPower';
break;
case chromeos.crostiniUpgrader.mojom.UpgradePrecheckStatus
.INSUFFICIENT_SPACE:
messageId = 'precheckNoSpace';
break;
default:
assertNotReached();
}
case State.UPGRADING:
messageId = 'upgradingMessage';
break;
......@@ -334,6 +388,7 @@ Polymer({
getIllustrationStyle_: function(state) {
switch (state) {
case State.BACKUP_SUCCEEDED:
case State.PRECHECKS_FAILED:
case State.ERROR:
return 'img-square-illustration';
}
......@@ -349,6 +404,7 @@ Polymer({
switch (state) {
case State.BACKUP_SUCCEEDED:
return 'images/success_illustration.png';
case State.PRECHECKS_FAILED:
case State.ERROR:
return 'images/error_illustration.png';
}
......
......@@ -4,6 +4,18 @@
module chromeos.crostini_upgrader.mojom;
enum UpgradePrecheckStatus {
// Good to continue
OK,
// No network connectivity
NETWORK_FAILURE,
// Battery is low and not charging
LOW_POWER,
// Not enough space to do the upgrade
INSUFFICIENT_SPACE
};
// Lives in the browser process. A renderer uses this to create a page handler
// for controlling Crostini upgrade.
interface PageHandlerFactory {
......@@ -17,6 +29,9 @@ interface PageHandlerFactory {
interface PageHandler {
// Backup the existing container.
Backup();
// Start running upgrade prechecks. Result is asynchronously
// returned via Page::PrecheckStatus.
StartPrechecks();
// Start upgrade
Upgrade();
// Cancel an on-going upgrade
......@@ -43,6 +58,8 @@ interface Page {
OnBackupSucceeded();
// This is called when the backup failed.
OnBackupFailed();
// Handle the result of the prechecks.
PrecheckStatus(UpgradePrecheckStatus status);
// Callback to receive the upgrade progress once the upgrade has started.
OnUpgradeProgress(array<string> progress_messages);
// This is called when the upgrade succeeded.
......
......@@ -34,6 +34,10 @@ void CrostiniUpgraderPageHandler::Backup() {
upgrader_ui_delegate_->Backup();
}
void CrostiniUpgraderPageHandler::StartPrechecks() {
upgrader_ui_delegate_->StartPrechecks();
}
void CrostiniUpgraderPageHandler::Upgrade() {
upgrader_ui_delegate_->Upgrade(
crostini::ContainerId(crostini::kCrostiniDefaultVmName,
......@@ -81,6 +85,11 @@ void CrostiniUpgraderPageHandler::OnBackupFailed() {
page_->OnBackupFailed();
}
void CrostiniUpgraderPageHandler::PrecheckStatus(
chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus status) {
page_->PrecheckStatus(status);
}
void CrostiniUpgraderPageHandler::OnCanceled() {
page_->OnCanceled();
}
......
......@@ -33,6 +33,7 @@ class CrostiniUpgraderPageHandler
// chromeos::crostini_upgrader::mojom::PageHandler:
void Backup() override;
void StartPrechecks() override;
void Upgrade() override;
void Cancel() override;
void CancelBeforeStart() override;
......@@ -43,6 +44,8 @@ class CrostiniUpgraderPageHandler
void OnBackupProgress(int percent) override;
void OnBackupSucceeded() override;
void OnBackupFailed() override;
void PrecheckStatus(chromeos::crostini_upgrader::mojom::UpgradePrecheckStatus
status) override;
void OnUpgradeProgress(const std::vector<std::string>& messages) override;
void OnUpgradeSucceeded() override;
void OnUpgradeFailed() override;
......
......@@ -22,6 +22,7 @@
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/web_ui_data_source.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/resources/grit/webui_resources.h"
#include "ui/strings/grit/ui_strings.h"
......@@ -38,6 +39,7 @@ bool CrostiniUpgraderUI::IsEnabled() {
void AddStringResources(content::WebUIDataSource* source) {
static constexpr webui::LocalizedString kStrings[] = {
{"upgrade", IDS_CROSTINI_UPGRADER_UPGRADE_BUTTON},
{"retry", IDS_CROSTINI_INSTALLER_RETRY_BUTTON},
{"close", IDS_APP_CLOSE},
{"cancel", IDS_APP_CANCEL},
{"launch", IDS_CROSTINI_UPGRADER_LAUNCH_BUTTON},
......@@ -46,11 +48,15 @@ void AddStringResources(content::WebUIDataSource* source) {
{"promptTitle", IDS_CROSTINI_UPGRADER_TITLE},
{"backingUpTitle", IDS_CROSTINI_UPGRADER_BACKING_UP_TITLE},
{"backupSucceededTitle", IDS_CROSTINI_UPGRADER_BACKUP_SUCCEEDED_TITLE},
{"prechecksFailedTitle", IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_TITLE},
{"upgradingTitle", IDS_CROSTINI_UPGRADER_UPGRADING_TITLE},
{"succeededTitle", IDS_CROSTINI_UPGRADER_SUCCEEDED_TITLE},
{"cancelingTitle", IDS_CROSTINI_UPGRADER_CANCELING_TITLE},
{"errorTitle", IDS_CROSTINI_UPGRADER_ERROR_TITLE},
{"precheckNoNetwork", IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_NETWORK},
{"precheckNoPower", IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_POWER},
{"promptMessage", IDS_CROSTINI_UPGRADER_BODY},
{"backingUpMessage", IDS_CROSTINI_UPGRADER_BACKING_UP_MESSAGE},
{"backupSucceededMessage",
......@@ -71,6 +77,12 @@ void AddStringResources(content::WebUIDataSource* source) {
source->AddString("offlineError",
l10n_util::GetStringFUTF8(
IDS_CROSTINI_INSTALLER_OFFLINE_ERROR, device_name));
source->AddString("precheckNoSpace",
l10n_util::GetStringFUTF8(
IDS_CROSTINI_UPGRADER_PRECHECKS_FAILED_SPACE,
ui::FormatBytesWithUnits(
crostini::CrostiniUpgrader::kDiskRequired,
ui::DATA_UNITS_GIBIBYTE, /*show_units=*/true)));
}
CrostiniUpgraderUI::CrostiniUpgraderUI(content::WebUI* web_ui)
......
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