| // 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/chromeos/crostini/crostini_installer.h" |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/ranges.h" |
| #include "base/strings/string16.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/post_task.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/chromeos/crostini/crostini_installer_types.mojom.h" |
| #include "chrome/browser/chromeos/crostini/crostini_manager_factory.h" |
| #include "chrome/browser/chromeos/crostini/crostini_pref_names.h" |
| #include "chrome/browser/chromeos/crostini/crostini_terminal.h" |
| #include "chrome/browser/chromeos/crostini/crostini_util.h" |
| #include "chrome/browser/chromeos/login/startup_utils.h" |
| #include "chrome/browser/chromeos/profiles/profile_helper.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "components/keyed_service/content/browser_context_keyed_service_factory.h" |
| #include "components/keyed_service/core/keyed_service.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/network_service_instance.h" |
| |
| using crostini::mojom::InstallerError; |
| using crostini::mojom::InstallerState; |
| |
| namespace crostini { |
| |
| namespace { |
| using SetupResult = CrostiniInstaller::SetupResult; |
| |
| class CrostiniInstallerFactory : public BrowserContextKeyedServiceFactory { |
| public: |
| static crostini::CrostiniInstaller* GetForProfile(Profile* profile) { |
| return static_cast<crostini::CrostiniInstaller*>( |
| GetInstance()->GetServiceForBrowserContext(profile, true)); |
| } |
| |
| static CrostiniInstallerFactory* GetInstance() { |
| static base::NoDestructor<CrostiniInstallerFactory> factory; |
| return factory.get(); |
| } |
| |
| private: |
| friend class base::NoDestructor<CrostiniInstallerFactory>; |
| |
| CrostiniInstallerFactory() |
| : BrowserContextKeyedServiceFactory( |
| "CrostiniInstallerService", |
| BrowserContextDependencyManager::GetInstance()) { |
| DependsOn(crostini::CrostiniManagerFactory::GetInstance()); |
| } |
| |
| // BrowserContextKeyedServiceFactory: |
| KeyedService* BuildServiceInstanceFor( |
| content::BrowserContext* context) const override { |
| Profile* profile = Profile::FromBrowserContext(context); |
| return new crostini::CrostiniInstaller(profile); |
| } |
| }; |
| |
| constexpr int kUninitializedDiskSpace = -1; |
| |
| constexpr char kCrostiniSetupResultHistogram[] = "Crostini.SetupResult"; |
| constexpr char kCrostiniTimeFromDeviceSetupToInstall[] = |
| "Crostini.TimeFromDeviceSetupToInstall"; |
| constexpr char kCrostiniTimeToInstallSuccess[] = |
| "Crostini.TimeToInstallSuccess"; |
| constexpr char kCrostiniTimeToInstallCancel[] = "Crostini.TimeToInstallCancel"; |
| constexpr char kCrostiniTimeToInstallError[] = "Crostini.TimeToInstallError"; |
| constexpr char kCrostiniAvailableDiskSuccess[] = |
| "Crostini.AvailableDiskSuccess"; |
| constexpr char kCrostiniAvailableDiskCancel[] = "Crostini.AvailableDiskCancel"; |
| constexpr char kCrostiniAvailableDiskError[] = "Crostini.AvailableDiskError"; |
| |
| void RecordTimeFromDeviceSetupToInstallMetric() { |
| base::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::ThreadPool(), base::MayBlock()}, |
| base::BindOnce(&chromeos::StartupUtils::GetTimeSinceOobeFlagFileCreation), |
| base::BindOnce([](base::TimeDelta time_from_device_setup) { |
| if (time_from_device_setup.is_zero()) |
| return; |
| |
| // The magic number 1471228928 is used for legacy reasons and changing |
| // it would invalidate already logged data. |
| base::UmaHistogramCustomTimes( |
| kCrostiniTimeFromDeviceSetupToInstall, time_from_device_setup, |
| base::TimeDelta::FromMinutes(1), |
| base::TimeDelta::FromMilliseconds(1471228928), 50); |
| })); |
| } |
| |
| SetupResult ErrorToSetupResult(InstallerError error) { |
| switch (error) { |
| case InstallerError::kNone: |
| return SetupResult::kSuccess; |
| case InstallerError::kErrorLoadingTermina: |
| return SetupResult::kErrorLoadingTermina; |
| case InstallerError::kErrorStartingConcierge: |
| return SetupResult::kErrorStartingConcierge; |
| case InstallerError::kErrorCreatingDiskImage: |
| return SetupResult::kErrorCreatingDiskImage; |
| case InstallerError::kErrorStartingTermina: |
| return SetupResult::kErrorStartingTermina; |
| case InstallerError::kErrorStartingContainer: |
| return SetupResult::kErrorStartingContainer; |
| case InstallerError::kErrorOffline: |
| return SetupResult::kErrorOffline; |
| case InstallerError::kErrorFetchingSshKeys: |
| return SetupResult::kErrorFetchingSshKeys; |
| case InstallerError::kErrorMountingContainer: |
| return SetupResult::kErrorMountingContainer; |
| case InstallerError::kErrorSettingUpContainer: |
| return SetupResult::kErrorSettingUpContainer; |
| case InstallerError::kErrorInsufficientDiskSpace: |
| return SetupResult::kErrorInsufficientDiskSpace; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| SetupResult InstallStateToCancelledSetupResult( |
| InstallerState installing_state) { |
| switch (installing_state) { |
| case InstallerState::kStart: |
| return SetupResult::kUserCancelledStart; |
| case InstallerState::kInstallImageLoader: |
| return SetupResult::kUserCancelledInstallImageLoader; |
| case InstallerState::kStartConcierge: |
| return SetupResult::kUserCancelledStartConcierge; |
| case InstallerState::kCreateDiskImage: |
| return SetupResult::kUserCancelledCreateDiskImage; |
| case InstallerState::kStartTerminaVm: |
| return SetupResult::kUserCancelledStartTerminaVm; |
| case InstallerState::kCreateContainer: |
| return SetupResult::kUserCancelledCreateContainer; |
| case InstallerState::kSetupContainer: |
| return SetupResult::kUserCancelledSetupContainer; |
| case InstallerState::kStartContainer: |
| return SetupResult::kUserCancelledStartContainer; |
| case InstallerState::kFetchSshKeys: |
| return SetupResult::kUserCancelledFetchSshKeys; |
| case InstallerState::kMountContainer: |
| return SetupResult::kUserCancelledMountContainer; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| } // namespace |
| |
| CrostiniInstaller* CrostiniInstaller::GetForProfile(Profile* profile) { |
| return CrostiniInstallerFactory::GetForProfile(profile); |
| } |
| |
| CrostiniInstaller::CrostiniInstaller(Profile* profile) : profile_(profile) {} |
| |
| CrostiniInstaller::~CrostiniInstaller() { |
| // Guaranteed by |Shutdown()|. |
| DCHECK_EQ(restart_id_, CrostiniManager::kUninitializedRestartId); |
| } |
| |
| void CrostiniInstaller::Shutdown() { |
| if (restart_id_ != CrostiniManager::kUninitializedRestartId) { |
| CrostiniManager::GetForProfile(profile_)->AbortRestartCrostini( |
| restart_id_, base::DoNothing()); |
| restart_id_ = CrostiniManager::kUninitializedRestartId; |
| } |
| } |
| |
| void CrostiniInstaller::Install(ProgressCallback progress_callback, |
| ResultCallback result_callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!CanInstall()) { |
| LOG(ERROR) |
| << "Tried to start crostini installation in invalid state. state_=" |
| << static_cast<int>(state_); |
| return; |
| } |
| |
| progress_callback_ = std::move(progress_callback); |
| result_callback_ = std::move(result_callback); |
| |
| install_start_time_ = base::TimeTicks::Now(); |
| require_cleanup_ = true; |
| free_disk_space_ = kUninitializedDiskSpace; |
| container_download_percent_ = 0; |
| UpdateState(State::INSTALLING); |
| |
| base::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::ThreadPool(), base::MayBlock()}, |
| base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace, |
| base::FilePath(crostini::kHomeDirectory)), |
| base::BindOnce(&CrostiniInstaller::OnAvailableDiskSpace, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // The default value of kCrostiniContainers is set to migrate existing |
| // crostini users who don't have the pref set. If crostini is being installed, |
| // then we know the user must not actually have any containers yet, so we set |
| // this pref to the empty list. |
| profile_->GetPrefs()->Set(crostini::prefs::kCrostiniContainers, |
| base::Value(base::Value::Type::LIST)); |
| } |
| |
| void CrostiniInstaller::Cancel(base::OnceClosure callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (state_ != State::INSTALLING) { |
| LOG(ERROR) << "Tried to cancel in non-cancelable state. state_=" |
| << static_cast<int>(state_); |
| return; |
| } |
| |
| UMA_HISTOGRAM_LONG_TIMES(kCrostiniTimeToInstallCancel, |
| base::TimeTicks::Now() - install_start_time_); |
| |
| result_callback_.Reset(); |
| progress_callback_.Reset(); |
| cancel_callback_ = std::move(callback); |
| |
| if (installing_state_ == InstallerState::kStart) { |
| // We have not called |RestartCrostini()| yet. |
| DCHECK_EQ(restart_id_, CrostiniManager::kUninitializedRestartId); |
| // OnAvailableDiskSpace() will take care of |cancel_callback_|. |
| UpdateState(State::CANCEL_ABORT_CHECK_DISK); |
| return; |
| } |
| |
| DCHECK_NE(restart_id_, CrostiniManager::kUninitializedRestartId); |
| |
| if (free_disk_space_ != kUninitializedDiskSpace) { |
| base::UmaHistogramCounts1M(kCrostiniAvailableDiskCancel, |
| free_disk_space_ >> 20); |
| } |
| |
| // Abort the long-running flow, and RestartObserver methods will not be called |
| // again until next installation. |
| crostini::CrostiniManager::GetForProfile(profile_)->AbortRestartCrostini( |
| restart_id_, base::DoNothing()); |
| restart_id_ = CrostiniManager::kUninitializedRestartId; |
| RecordSetupResult(InstallStateToCancelledSetupResult(installing_state_)); |
| |
| if (require_cleanup_) { |
| // Remove anything that got installed |
| base::PostTask( |
| FROM_HERE, {content::BrowserThread::UI}, |
| base::BindOnce(&crostini::CrostiniManager::RemoveCrostini, |
| base::Unretained( |
| crostini::CrostiniManager::GetForProfile(profile_)), |
| crostini::kCrostiniDefaultVmName, |
| base::BindOnce(&CrostiniInstaller::FinishCleanup, |
| weak_ptr_factory_.GetWeakPtr()))); |
| UpdateState(State::CANCEL_CLEANUP); |
| } else { |
| base::PostTask(FROM_HERE, {content::BrowserThread::UI}, |
| std::move(cancel_callback_)); |
| UpdateState(State::IDLE); |
| } |
| } |
| |
| void CrostiniInstaller::CancelBeforeStart() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!CanInstall()) { |
| LOG(ERROR) << "Not in pre-install state. state_=" |
| << static_cast<int>(state_); |
| return; |
| } |
| RecordSetupResult(SetupResult::kNotStarted); |
| } |
| |
| void CrostiniInstaller::OnComponentLoaded(CrostiniResult result) { |
| DCHECK_EQ(installing_state_, InstallerState::kInstallImageLoader); |
| |
| if (result != CrostiniResult::SUCCESS) { |
| if (content::GetNetworkConnectionTracker()->IsOffline()) { |
| LOG(ERROR) << "Network connection dropped while downloading cros-termina"; |
| HandleError(InstallerError::kErrorOffline); |
| } else { |
| LOG(ERROR) << "Failed to install the cros-termina component"; |
| HandleError(InstallerError::kErrorLoadingTermina); |
| } |
| return; |
| } |
| UpdateInstallingState(InstallerState::kStartConcierge); |
| } |
| |
| void CrostiniInstaller::OnConciergeStarted(bool success) { |
| DCHECK_EQ(installing_state_, InstallerState::kStartConcierge); |
| if (!success) { |
| HandleError(InstallerError::kErrorStartingConcierge); |
| return; |
| } |
| UpdateInstallingState(InstallerState::kCreateDiskImage); |
| } |
| |
| void CrostiniInstaller::OnDiskImageCreated( |
| bool success, |
| vm_tools::concierge::DiskImageStatus status, |
| int64_t disk_size_available) { |
| DCHECK_EQ(installing_state_, InstallerState::kCreateDiskImage); |
| if (!success) { |
| HandleError(InstallerError::kErrorCreatingDiskImage); |
| return; |
| } |
| if (status == vm_tools::concierge::DiskImageStatus::DISK_STATUS_EXISTS) { |
| require_cleanup_ = false; |
| } |
| UpdateInstallingState(InstallerState::kStartTerminaVm); |
| } |
| |
| void CrostiniInstaller::OnVmStarted(bool success) { |
| DCHECK_EQ(installing_state_, InstallerState::kStartTerminaVm); |
| if (!success) { |
| HandleError(InstallerError::kErrorStartingTermina); |
| return; |
| } |
| UpdateInstallingState(InstallerState::kCreateContainer); |
| } |
| |
| void CrostiniInstaller::OnContainerDownloading(int32_t download_percent) { |
| DCHECK_EQ(installing_state_, InstallerState::kCreateContainer); |
| container_download_percent_ = base::ClampToRange(download_percent, 0, 100); |
| RunProgressCallback(); |
| } |
| |
| void CrostiniInstaller::OnContainerCreated(CrostiniResult result) { |
| DCHECK_EQ(installing_state_, InstallerState::kCreateContainer); |
| UpdateInstallingState(InstallerState::kSetupContainer); |
| } |
| |
| void CrostiniInstaller::OnContainerSetup(bool success) { |
| DCHECK_EQ(installing_state_, InstallerState::kSetupContainer); |
| |
| if (!success) { |
| if (content::GetNetworkConnectionTracker()->IsOffline()) { |
| LOG(ERROR) << "Network connection dropped while downloading container"; |
| HandleError(InstallerError::kErrorOffline); |
| } else { |
| HandleError(InstallerError::kErrorSettingUpContainer); |
| } |
| return; |
| } |
| UpdateInstallingState(InstallerState::kStartContainer); |
| } |
| |
| void CrostiniInstaller::OnContainerStarted(CrostiniResult result) { |
| DCHECK_EQ(installing_state_, InstallerState::kStartContainer); |
| |
| if (result != CrostiniResult::SUCCESS) { |
| LOG(ERROR) << "Failed to start container with error code: " |
| << static_cast<int>(result); |
| HandleError(InstallerError::kErrorStartingContainer); |
| return; |
| } |
| UpdateInstallingState(InstallerState::kFetchSshKeys); |
| } |
| |
| void CrostiniInstaller::OnSshKeysFetched(bool success) { |
| DCHECK_EQ(installing_state_, InstallerState::kFetchSshKeys); |
| |
| if (!success) { |
| HandleError(InstallerError::kErrorFetchingSshKeys); |
| return; |
| } |
| UpdateInstallingState(InstallerState::kMountContainer); |
| } |
| |
| bool CrostiniInstaller::CanInstall() { |
| // Allow to start from State::ERROR. In that case, we're doing a Retry. |
| return state_ == State::IDLE || state_ == State::ERROR; |
| } |
| |
| void CrostiniInstaller::RunProgressCallback() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK_EQ(state_, State::INSTALLING); |
| |
| base::TimeDelta time_in_state = |
| base::Time::Now() - installing_state_start_time_; |
| |
| double state_start_mark = 0; |
| double state_end_mark = 0; |
| int state_max_seconds = 1; |
| |
| switch (installing_state_) { |
| case InstallerState::kStart: |
| state_start_mark = 0; |
| state_end_mark = 0; |
| break; |
| case InstallerState::kInstallImageLoader: |
| state_start_mark = 0.0; |
| state_end_mark = 0.25; |
| state_max_seconds = 30; |
| break; |
| case InstallerState::kStartConcierge: |
| state_start_mark = 0.25; |
| state_end_mark = 0.26; |
| break; |
| case InstallerState::kCreateDiskImage: |
| state_start_mark = 0.26; |
| state_end_mark = 0.27; |
| break; |
| case InstallerState::kStartTerminaVm: |
| state_start_mark = 0.27; |
| state_end_mark = 0.35; |
| state_max_seconds = 8; |
| break; |
| case InstallerState::kCreateContainer: |
| state_start_mark = 0.35; |
| state_end_mark = 0.90; |
| state_max_seconds = 180; |
| break; |
| case InstallerState::kSetupContainer: |
| state_start_mark = 0.90; |
| state_end_mark = 0.95; |
| state_max_seconds = 8; |
| break; |
| case InstallerState::kStartContainer: |
| state_start_mark = 0.95; |
| state_end_mark = 0.99; |
| state_max_seconds = 8; |
| break; |
| case InstallerState::kFetchSshKeys: |
| state_start_mark = 0.99; |
| state_end_mark = 1; |
| break; |
| case InstallerState::kMountContainer: |
| state_start_mark = 1; |
| state_end_mark = 1; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| |
| double state_fraction = time_in_state.InSecondsF() / state_max_seconds; |
| |
| if (installing_state_ == InstallerState::kCreateContainer) { |
| // In CREATE_CONTAINER, consume half the progress bar with downloading, |
| // the rest with time. |
| state_fraction = |
| 0.5 * (state_fraction + 0.01 * container_download_percent_); |
| } |
| |
| double progress = |
| state_start_mark + base::ClampToRange(state_fraction, 0.0, 1.0) * |
| (state_end_mark - state_start_mark); |
| progress_callback_.Run(installing_state_, progress); |
| } |
| |
| void CrostiniInstaller::UpdateState(State new_state) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK_NE(state_, new_state); |
| |
| state_ = new_state; |
| if (state_ == State::INSTALLING) { |
| // We are not calling the progress callback here because 1) there is nothing |
| // interesting to report; 2) We reach here from |Install()|, so calling the |
| // callback risk reentering |Install()|'s caller. |
| UpdateInstallingState(InstallerState::kStart, |
| /*run_callback=*/false); |
| |
| state_progress_timer_.Start( |
| FROM_HERE, base::TimeDelta::FromMilliseconds(500), |
| base::BindRepeating(&CrostiniInstaller::RunProgressCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| state_progress_timer_.AbandonAndStop(); |
| } |
| } |
| |
| void CrostiniInstaller::UpdateInstallingState( |
| InstallerState new_installing_state, |
| bool run_callback) { |
| DCHECK_EQ(state_, State::INSTALLING); |
| installing_state_start_time_ = base::Time::Now(); |
| installing_state_ = new_installing_state; |
| |
| if (run_callback) { |
| RunProgressCallback(); |
| } |
| } |
| |
| void CrostiniInstaller::HandleError(InstallerError error) { |
| DCHECK_EQ(state_, State::INSTALLING); |
| DCHECK_NE(error, InstallerError::kNone); |
| |
| UMA_HISTOGRAM_LONG_TIMES(kCrostiniTimeToInstallError, |
| base::TimeTicks::Now() - install_start_time_); |
| if (free_disk_space_ != kUninitializedDiskSpace) { |
| base::UmaHistogramCounts1M(kCrostiniAvailableDiskError, |
| free_disk_space_ >> 20); |
| } |
| |
| RecordSetupResult(ErrorToSetupResult(error)); |
| |
| // |restart_id_| is reset in |OnCrostiniRestartFinished()|. |
| UpdateState(State::ERROR); |
| |
| std::move(result_callback_).Run(error); |
| progress_callback_.Reset(); |
| } |
| |
| void CrostiniInstaller::FinishCleanup(crostini::CrostiniResult result) { |
| if (result != CrostiniResult::SUCCESS) { |
| LOG(ERROR) << "Failed to cleanup aborted crostini install"; |
| } |
| UpdateState(State::IDLE); |
| std::move(cancel_callback_).Run(); |
| } |
| |
| void CrostiniInstaller::RecordSetupResult(SetupResult result) { |
| base::UmaHistogramEnumeration(kCrostiniSetupResultHistogram, result); |
| } |
| |
| void CrostiniInstaller::OnCrostiniRestartFinished(CrostiniResult result) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| restart_id_ = CrostiniManager::kUninitializedRestartId; |
| |
| if (result != CrostiniResult::SUCCESS) { |
| if (state_ != State::ERROR) { |
| DCHECK_EQ(state_, State::INSTALLING); |
| LOG(ERROR) << "Failed to restart Crostini with error code: " |
| << static_cast<int>(result); |
| // TODO(lxj): The error code here is probably incorrect. If |
| // |CrostiniManager::CrostiniRestarter| failed to mount the container, it |
| // still calls this function with |SUCCESS| (see |
| // |CrostiniRestarter::OnMountEvent()|), so if we reach here (i.e. |
| // |state_| has not been set to |ERROR| but this function receives a |
| // failure result), something else is probably wrong. |
| HandleError(InstallerError::kErrorMountingContainer); |
| } |
| return; |
| } |
| |
| DCHECK_EQ(installing_state_, InstallerState::kMountContainer); |
| // Reset state to allow |Install()| again in case the user remove and |
| // re-install crostini. |
| UpdateState(State::IDLE); |
| |
| RecordSetupResult(SetupResult::kSuccess); |
| crostini::CrostiniManager::GetForProfile(profile_) |
| ->UpdateLaunchMetricsForEnterpriseReporting(); |
| RecordTimeFromDeviceSetupToInstallMetric(); |
| UMA_HISTOGRAM_LONG_TIMES(kCrostiniTimeToInstallSuccess, |
| base::TimeTicks::Now() - install_start_time_); |
| if (free_disk_space_ != kUninitializedDiskSpace) { |
| base::UmaHistogramCounts1M(kCrostiniAvailableDiskSuccess, |
| free_disk_space_ >> 20); |
| } |
| |
| std::move(result_callback_).Run(InstallerError::kNone); |
| progress_callback_.Reset(); |
| |
| if (!skip_launching_terminal_for_testing_) { |
| crostini::LaunchContainerTerminal( |
| profile_, crostini::kCrostiniDefaultVmName, |
| crostini::kCrostiniDefaultContainerName, std::vector<std::string>()); |
| } |
| } |
| |
| void CrostiniInstaller::OnAvailableDiskSpace(int64_t bytes) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // |Cancel()| might be called immediately after |Install()|. |
| if (state_ == State::CANCEL_ABORT_CHECK_DISK) { |
| UpdateState(State::IDLE); |
| RecordSetupResult(SetupResult::kNotStarted); |
| std::move(cancel_callback_).Run(); |
| return; |
| } |
| |
| DCHECK_EQ(installing_state_, InstallerState::kStart); |
| |
| free_disk_space_ = bytes; |
| // Don't enforce minimum disk size on dev box or trybots because |
| // base::SysInfo::AmountOfFreeDiskSpace returns zero in testing. |
| if (free_disk_space_ < kMinimumFreeDiskSpace && |
| base::SysInfo::IsRunningOnChromeOS()) { |
| HandleError(InstallerError::kErrorInsufficientDiskSpace); |
| return; |
| } |
| |
| if (content::GetNetworkConnectionTracker()->IsOffline()) { |
| HandleError(InstallerError::kErrorOffline); |
| return; |
| } |
| |
| UpdateInstallingState(InstallerState::kInstallImageLoader); |
| |
| // Kick off the Crostini Restart sequence. We will be added as an observer. |
| restart_id_ = |
| crostini::CrostiniManager::GetForProfile(profile_)->RestartCrostini( |
| crostini::kCrostiniDefaultVmName, |
| crostini::kCrostiniDefaultContainerName, |
| base::BindOnce(&CrostiniInstaller::OnCrostiniRestartFinished, |
| weak_ptr_factory_.GetWeakPtr()), |
| this); |
| |
| // |restart_id| will be invalid when |CrostiniManager::RestartCrostini()| |
| // decides to fail immediately and calls |OnCrostiniRestartFinished()|, which |
| // subsequently set |state_| to |ERROR|. |
| DCHECK_EQ(restart_id_ == CrostiniManager::kUninitializedRestartId, |
| state_ == State::ERROR); |
| } |
| |
| } // namespace crostini |