blob: 2e1f917ebcf041bbf100191d97933a5147772c3f [file] [log] [blame]
// Copyright 2020 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/ash/crosapi/browser_loader.h"
#include <utility>
#include "ash/constants/ash_switches.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/ash/system_tray_client_impl.h"
#include "chromeos/cryptohome/system_salt_getter.h"
#include "components/component_updater/component_updater_service.h"
namespace crosapi {
namespace {
// Emergency kill switch in case the notification code doesn't work properly.
const base::Feature kLacrosShowUpdateNotifications{
"LacrosShowUpdateNotifications", base::FEATURE_ENABLED_BY_DEFAULT};
// The rootfs lacros-chrome binary related files.
constexpr char kLacrosChromeBinary[] = "chrome";
constexpr char kLacrosMetadata[] = "metadata.json";
// The rootfs lacros-chrome binary related paths.
// Must be kept in sync with lacros upstart conf files.
constexpr char kRootfsLacrosMountPoint[] = "/run/lacros";
constexpr char kRootfsLacrosPath[] = "/opt/google/lacros";
// Lacros upstart jobs for mounting/unmounting the lacros-chrome image.
// The conversion of upstart job names to dbus object paths is undocumented. See
// function nih_dbus_path in libnih for the implementation.
constexpr char kLacrosMounterUpstartJob[] = "lacros_2dmounter";
constexpr char kLacrosUnmounterUpstartJob[] = "lacros_2dunmounter";
std::string GetLacrosComponentName() {
return browser_util::GetLacrosComponentInfo().name;
}
// Returns the CRX "extension" ID for a lacros component.
std::string GetLacrosComponentCrxId() {
return browser_util::GetLacrosComponentInfo().crx_id;
}
// Returns whether lacros-chrome component is registered.
bool CheckRegisteredMayBlock(
scoped_refptr<component_updater::CrOSComponentManager> manager) {
return manager->IsRegisteredMayBlock(GetLacrosComponentName());
}
// Returns whether any lacros-chrome component is registered.
bool CheckAnyRegisteredMayBlock(
scoped_refptr<component_updater::CrOSComponentManager> manager) {
for (const auto& component_info : {browser_util::kLacrosDogfoodCanaryInfo,
browser_util::kLacrosDogfoodDevInfo,
browser_util::kLacrosDogfoodBetaInfo,
browser_util::kLacrosDogfoodStableInfo}) {
if (manager->IsRegisteredMayBlock(component_info.name))
return true;
}
return false;
}
// Returns whether lacros-fishfood component is already installed.
// If it is, delete the user directory, too, because it will be
// uninstalled.
bool CheckInstalledAndMaybeRemoveUserDirectory(
scoped_refptr<component_updater::CrOSComponentManager> manager) {
if (!CheckRegisteredMayBlock(manager))
return false;
// Since we're already on a background thread, delete the user-data-dir
// associated with lacros.
// TODO(hidehiko): This approach has timing issue. Specifically, if Chrome
// shuts down during the directory remove, some partially-removed directory
// may be kept, and if the user flips the flag in the next time, that
// partially-removed directory could be used. Fix this.
base::DeletePathRecursively(browser_util::GetUserDataDir());
return true;
}
// Production delegate implementation.
class DelegateImpl : public BrowserLoader::Delegate {
public:
DelegateImpl() = default;
DelegateImpl(const DelegateImpl&) = delete;
DelegateImpl& operator=(const DelegateImpl&) = delete;
~DelegateImpl() override = default;
// BrowserLoader::Delegate:
void SetLacrosUpdateAvailable() override {
if (base::FeatureList::IsEnabled(kLacrosShowUpdateNotifications)) {
// Show the update notification in ash.
SystemTrayClientImpl::Get()->SetLacrosUpdateAvailable();
}
}
};
} // namespace
BrowserLoader::BrowserLoader(
scoped_refptr<component_updater::CrOSComponentManager> manager)
: BrowserLoader(std::make_unique<DelegateImpl>(),
manager,
g_browser_process->component_updater(),
chromeos::UpstartClient::Get()) {}
BrowserLoader::BrowserLoader(
std::unique_ptr<Delegate> delegate,
scoped_refptr<component_updater::CrOSComponentManager> manager,
component_updater::ComponentUpdateService* updater,
chromeos::UpstartClient* upstart_client)
: delegate_(std::move(delegate)),
component_manager_(manager),
component_update_service_(updater),
upstart_client_(upstart_client) {
DCHECK(delegate_);
DCHECK(component_manager_);
}
BrowserLoader::~BrowserLoader() {
// May be null in tests.
if (component_update_service_) {
// Removing an observer is a no-op if the observer wasn't added.
component_update_service_->RemoveObserver(this);
}
}
void BrowserLoader::Load(LoadCompletionCallback callback) {
DCHECK(browser_util::IsLacrosEnabled());
// TODO(crbug.com/1078607): Remove non-error logging from this class.
LOG(WARNING) << "Starting lacros component load.";
// If the user has specified a path for the lacros-chrome binary, use that
// rather than component manager.
base::FilePath lacros_chrome_path =
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
chromeos::switches::kLacrosChromePath);
if (!lacros_chrome_path.empty()) {
OnLoadComplete(std::move(callback),
component_updater::CrOSComponentManager::Error::NONE,
lacros_chrome_path);
return;
}
// If the user has specified to force using stateful or rootfs lacros-chrome
// binary, force the selection.
const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
if (cmdline->HasSwitch(browser_util::kLacrosSelectionSwitch)) {
auto value =
cmdline->GetSwitchValueASCII(browser_util::kLacrosSelectionSwitch);
if (value == browser_util::kLacrosSelectionRootfs) {
LoadRootfsLacros(std::move(callback));
return;
}
if (value == browser_util::kLacrosSelectionStateful) {
LoadStatefulLacros(std::move(callback));
return;
}
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&CheckAnyRegisteredMayBlock, component_manager_),
base::BindOnce(&BrowserLoader::OnLoadSelection,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void BrowserLoader::OnLoadSelection(LoadCompletionCallback callback,
bool any_lacros_component_registered) {
// If there currently isn't a stateful lacros-chrome binary, proceed to use
// the rootfs lacros-chrome binary and start the installation of the
// stateful lacros-chrome binary in the background.
if (!any_lacros_component_registered) {
LoadRootfsLacros(std::move(callback));
LoadStatefulLacros({});
return;
}
// Otherwise proceed to load/mount the stateful lacros-chrome binary.
// In the case that the stateful lacros-chrome binary wasn't installed, this
// might take some time.
component_manager_->Load(
GetLacrosComponentName(),
component_updater::CrOSComponentManager::MountPolicy::kMount,
component_updater::CrOSComponentManager::UpdatePolicy::kDontForce,
base::BindOnce(&BrowserLoader::OnLoadSelectionMountStateful,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void BrowserLoader::OnLoadSelectionMountStateful(
LoadCompletionCallback callback,
component_updater::CrOSComponentManager::Error error,
const base::FilePath& path) {
if (error != component_updater::CrOSComponentManager::Error::NONE ||
path.empty()) {
LOG(WARNING) << "Error loading lacros component image: "
<< static_cast<int>(error);
std::move(callback).Run(base::FilePath(), LacrosSelection::kStateful);
return;
}
// Proceed to compare the lacros-chrome binary versions in case rootfs
// lacros-chrome binary is newer than stateful lacros-chrome binary.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&browser_util::GetRootfsLacrosVersionMayBlock,
base::FilePath(kRootfsLacrosPath).Append(kLacrosMetadata)),
base::BindOnce(&BrowserLoader::OnLoadVersionSelection,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void BrowserLoader::OnLoadVersionSelection(
LoadCompletionCallback callback,
base::Version rootfs_lacros_version) {
// Compare the rootfs vs stateful lacros-chrome binary versions.
// If the rootfs lacros-chrome is greater than or equal to the stateful
// lacros-chrome version, prioritize using the rootfs lacros-chrome and let
// stateful lacros-chrome update in the background.
if (rootfs_lacros_version.IsValid()) {
const auto lacros_component_name =
base::UTF8ToUTF16(base::StringPiece(GetLacrosComponentName()));
for (const auto& component_info :
component_update_service_->GetComponents()) {
if (component_info.name != lacros_component_name)
continue;
LOG(WARNING) << "Comparing lacros versions: "
<< "rootfs (" << rootfs_lacros_version.GetString() << "), "
<< "stateful " << lacros_component_name << " ("
<< component_info.version.GetString() << ")";
if (component_info.version <= rootfs_lacros_version) {
LOG(WARNING)
<< "Stateful lacros version is older or same as the one in rootfs, "
<< "proceeding to use rootfs lacros.";
LoadRootfsLacros(std::move(callback));
LoadStatefulLacros({});
return;
}
// Break out to use stateful lacros-chrome.
LOG(WARNING)
<< "Stateful lacros version is newer than the one in rootfs, "
<< "procceding to use stateful lacros.";
break;
}
}
LoadStatefulLacros(std::move(callback));
}
void BrowserLoader::LoadStatefulLacros(LoadCompletionCallback callback) {
LOG(WARNING) << "Loading stateful lacros.";
// Unmount the rootfs lacros-chrome if we want to use stateful lacros-chrome.
// This will keep stateful lacros-chrome only mounted and not hold the rootfs
// lacros-chrome mount until a `Unload`.
if (callback) {
// Ignore the unmount result.
upstart_client_->StartJob(kLacrosUnmounterUpstartJob, {},
base::BindOnce([](bool) {}));
}
component_manager_->Load(
GetLacrosComponentName(),
component_updater::CrOSComponentManager::MountPolicy::kMount,
// If a compatible installation exists, use that and download any updates
// in the background.
component_updater::CrOSComponentManager::UpdatePolicy::kDontForce,
// If `callback` is null, means stateful lacros-chrome should be
// installed/updated but rootfs lacros-chrome will be used.
callback
? base::BindOnce(&BrowserLoader::OnLoadComplete,
weak_factory_.GetWeakPtr(), std::move(callback))
: base::BindOnce([](component_updater::CrOSComponentManager::Error,
const base::FilePath& path) {}));
}
void BrowserLoader::LoadRootfsLacros(LoadCompletionCallback callback) {
LOG(WARNING) << "Loading rootfs lacros.";
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(
&base::PathExists,
base::FilePath(kRootfsLacrosMountPoint).Append(kLacrosChromeBinary)),
base::BindOnce(&BrowserLoader::OnLoadRootfsLacros,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void BrowserLoader::OnLoadRootfsLacros(LoadCompletionCallback callback,
bool already_mounted) {
if (already_mounted) {
OnUpstartLacrosMounter(std::move(callback), true);
return;
}
upstart_client_->StartJob(
kLacrosMounterUpstartJob, {},
base::BindOnce(&BrowserLoader::OnUpstartLacrosMounter,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void BrowserLoader::OnUpstartLacrosMounter(LoadCompletionCallback callback,
bool success) {
if (!success)
LOG(WARNING) << "Upstart failed to mount rootfs lacros.";
OnLoadComplete(
std::move(callback), component_updater::CrOSComponentManager::Error::NONE,
// If mounting wasn't successful, return a empty mount point to indicate
// failure. `OnLoadComplete` handles empty mount points and forwards the
// errors on the return callbacks.
success ? base::FilePath(kRootfsLacrosMountPoint) : base::FilePath());
}
void BrowserLoader::Unload() {
// Can be called even if Lacros isn't enabled, to clean up the old install.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&CheckInstalledAndMaybeRemoveUserDirectory,
component_manager_),
base::BindOnce(&BrowserLoader::OnCheckInstalled,
weak_factory_.GetWeakPtr()));
// Unmount the rootfs lacros-chrome if it was mounted.
// Ignore the unmount result.
upstart_client_->StartJob(kLacrosUnmounterUpstartJob, {},
base::BindOnce([](bool) {}));
}
void BrowserLoader::OnEvent(Events event, const std::string& id) {
// Check for the Lacros component being updated.
if (event == Events::COMPONENT_UPDATED && id == GetLacrosComponentCrxId()) {
delegate_->SetLacrosUpdateAvailable();
}
}
void BrowserLoader::OnLoadComplete(
LoadCompletionCallback callback,
component_updater::CrOSComponentManager::Error error,
const base::FilePath& path) {
LacrosSelection selection = LacrosSelection::kStateful;
if (path == base::FilePath(kRootfsLacrosPath) ||
path == base::FilePath(kRootfsLacrosMountPoint)) {
selection = LacrosSelection::kRootfs;
}
// Bail out on error or empty `path`.
if (error != component_updater::CrOSComponentManager::Error::NONE ||
path.empty()) {
LOG(WARNING) << "Error loading lacros component image: "
<< static_cast<int>(error);
std::move(callback).Run(base::FilePath(), selection);
return;
}
// Log the path on success.
LOG(WARNING) << "Loaded lacros image at " << path.MaybeAsASCII();
std::move(callback).Run(path, selection);
// May be null in tests.
if (component_update_service_) {
// Now that we have the initial component download, start observing for
// future updates. We don't do this in the constructor because we don't want
// to show the "update available" notification for the initial load.
component_update_service_->AddObserver(this);
}
}
void BrowserLoader::OnCheckInstalled(bool was_installed) {
if (!was_installed)
return;
// Workaround for login crash when the user un-sets the LacrosSupport flag.
// CrOSComponentManager::Unload() calls into code in MetadataTable that
// assumes that system salt is available. This isn't always true when chrome
// restarts to apply non-owner flags. It's hard to make MetadataTable async.
// Ensure salt is available before unloading. https://crbug.com/1122674
chromeos::SystemSaltGetter::Get()->GetSystemSalt(base::BindOnce(
&BrowserLoader::UnloadAfterCleanUp, weak_factory_.GetWeakPtr()));
}
void BrowserLoader::UnloadAfterCleanUp(const std::string& ignored_salt) {
CHECK(chromeos::SystemSaltGetter::Get()->GetRawSalt());
component_manager_->Unload(GetLacrosComponentName());
}
} // namespace crosapi