blob: 3a710b3fc0ce9dd9c2d8897839ce8ae3a86c7969 [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/chromeos/crosapi/browser_manager.h"
#include <fcntl.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/launch.h"
#include "base/process/process_handle.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/crosapi/ash_chrome_service_impl.h"
#include "chrome/browser/chromeos/crosapi/browser_loader.h"
#include "chrome/browser/chromeos/crosapi/browser_util.h"
#include "chrome/browser/chromeos/crosapi/environment_provider.h"
#include "chrome/browser/chromeos/crosapi/test_mojo_connection_manager.h"
#include "chrome/browser/component_updater/cros_component_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
#include "chromeos/constants/chromeos_switches.h"
#include "chromeos/startup/startup_switches.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "google_apis/google_api_keys.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/platform/platform_channel.h"
// TODO(crbug.com/1101667): Currently, this source has log spamming
// by LOG(WARNING) for non critical errors to make it easy
// to debug and develop. Get rid of the log spamming
// when it gets stable enough.
namespace crosapi {
namespace {
// Pointer to the global instance of BrowserManager.
BrowserManager* g_instance = nullptr;
// The min version of LacrosChromeService mojo interface that supports
// GetFeedbackData API.
constexpr uint32_t kGetFeedbackDataMinVersion = 6;
// The min version of LacrosChromeService mojo interface that supports
// GetHistograms API.
constexpr uint32_t kGetHistogramsMinVersion = 7;
// The min version of LacrosChromeService mojo interface that supports
// GetActiveTabUrl API.
constexpr uint32_t kGetActiveTabUrlMinVersion = 8;
base::FilePath LacrosLogPath() {
return browser_util::GetUserDataDir().Append("lacros.log");
}
base::ScopedFD CreateLogFile() {
base::FilePath::StringType log_path = LacrosLogPath().value();
// Delete old log file if exists.
if (unlink(log_path.c_str()) != 0) {
if (errno != ENOENT) {
// unlink() failed for reason other than the file not existing.
PLOG(ERROR) << "Failed to unlink the log file " << log_path;
return base::ScopedFD();
}
// If log file does not exist, most likely the user directory does not exist
// either. So create it here.
base::File::Error error;
if (!base::CreateDirectoryAndGetError(browser_util::GetUserDataDir(),
&error)) {
LOG(ERROR) << "Failed to make directory "
<< browser_util::GetUserDataDir()
<< base::File::ErrorToString(error);
return base::ScopedFD();
}
}
int fd =
HANDLE_EINTR(open(log_path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644));
if (fd < 0) {
PLOG(ERROR) << "Failed to get file descriptor for " << log_path;
return base::ScopedFD();
}
return base::ScopedFD(fd);
}
std::string GetXdgRuntimeDir() {
// If ash-chrome was given an environment variable, use it.
std::unique_ptr<base::Environment> env = base::Environment::Create();
std::string xdg_runtime_dir;
if (env->GetVar("XDG_RUNTIME_DIR", &xdg_runtime_dir))
return xdg_runtime_dir;
// Otherwise provide the default for Chrome OS devices.
return "/run/chrome";
}
void TerminateLacrosChrome(base::Process process) {
// Here, lacros-chrome process may crashed, or be in the shutdown procedure.
// Give some amount of time for the collection. In most cases,
// this wait captures the process termination.
constexpr base::TimeDelta kGracefulShutdownTimeout =
base::TimeDelta::FromSeconds(5);
if (process.WaitForExitWithTimeout(kGracefulShutdownTimeout, nullptr))
return;
// Here, the process is not yet terminated.
// This happens if some critical error happens on the mojo connection,
// while both ash-chrome and lacros-chrome are still alive.
// Terminate the lacros-chrome.
bool success = process.Terminate(/*exit_code=*/0, /*wait=*/true);
LOG_IF(ERROR, !success) << "Failed to terminate the lacros-chrome.";
}
void SetLaunchOnLoginPref(bool launch_on_login) {
ProfileManager::GetPrimaryUserProfile()->GetPrefs()->SetBoolean(
browser_util::kLaunchOnLoginPref, launch_on_login);
}
bool GetLaunchOnLoginPref() {
return ProfileManager::GetPrimaryUserProfile()->GetPrefs()->GetBoolean(
browser_util::kLaunchOnLoginPref);
}
} // namespace
// static
BrowserManager* BrowserManager::Get() {
return g_instance;
}
BrowserManager::BrowserManager(
scoped_refptr<component_updater::CrOSComponentManager> manager)
: component_manager_(manager),
environment_provider_(std::make_unique<EnvironmentProvider>()) {
DCHECK(!g_instance);
g_instance = this;
// Wait to query the flag until the user has entered the session. Enterprise
// devices restart Chrome during login to apply flags. We don't want to run
// the flag-off cleanup logic until we know we have the final flag state.
if (session_manager::SessionManager::Get())
session_manager::SessionManager::Get()->AddObserver(this);
std::string socket_path =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
chromeos::switches::kLacrosMojoSocketForTesting);
if (!socket_path.empty()) {
test_mojo_connection_manager_ =
std::make_unique<crosapi::TestMojoConnectionManager>(
base::FilePath(socket_path));
}
}
BrowserManager::~BrowserManager() {
// Unregister, just in case the manager is destroyed before
// OnUserSessionStarted() is called.
if (session_manager::SessionManager::Get())
session_manager::SessionManager::Get()->RemoveObserver(this);
// Try to kill the lacros-chrome binary.
if (lacros_process_.IsValid())
lacros_process_.Terminate(/*ignored=*/0, /*wait=*/false);
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
}
bool BrowserManager::IsReady() const {
return state_ != State::NOT_INITIALIZED && state_ != State::LOADING &&
state_ != State::UNAVAILABLE;
}
bool BrowserManager::IsRunning() const {
return state_ == State::RUNNING;
}
void BrowserManager::SetLoadCompleteCallback(LoadCompleteCallback callback) {
// We only support one client waiting.
DCHECK(!load_complete_callback_);
load_complete_callback_ = std::move(callback);
}
void BrowserManager::NewWindow() {
if (!browser_util::IsLacrosEnabled())
return;
if (!IsReady()) {
LOG(WARNING) << "lacros component image not yet available";
return;
}
DCHECK(!lacros_path_.empty());
if (state_ == State::TERMINATING) {
LOG(WARNING) << "lacros-chrome is terminating, so cannot start now";
return;
}
if (state_ == State::CREATING_LOG_FILE) {
LOG(WARNING) << "lacros-chrome is in the process of launching";
return;
}
if (state_ == State::STOPPED) {
// If lacros-chrome is not running, launch it.
Start();
return;
}
DCHECK(lacros_chrome_service_.is_connected());
lacros_chrome_service_->NewWindow(base::DoNothing());
}
bool BrowserManager::GetFeedbackDataSupported() const {
return lacros_chrome_service_version_ >= kGetFeedbackDataMinVersion;
}
void BrowserManager::GetFeedbackData(GetFeedbackDataCallback callback) {
DCHECK(lacros_chrome_service_.is_connected());
DCHECK(GetFeedbackDataSupported());
lacros_chrome_service_->GetFeedbackData(std::move(callback));
}
bool BrowserManager::GetHistogramsSupported() const {
return lacros_chrome_service_version_ >= kGetHistogramsMinVersion;
}
void BrowserManager::GetHistograms(GetHistogramsCallback callback) {
DCHECK(lacros_chrome_service_.is_connected());
DCHECK(GetHistogramsSupported());
lacros_chrome_service_->GetHistograms(std::move(callback));
}
bool BrowserManager::GetActiveTabUrlSupported() const {
return lacros_chrome_service_version_ >= kGetActiveTabUrlMinVersion;
}
void BrowserManager::GetActiveTabUrl(GetActiveTabUrlCallback callback) {
DCHECK(lacros_chrome_service_.is_connected());
DCHECK(GetActiveTabUrlSupported());
lacros_chrome_service_->GetActiveTabUrl(std::move(callback));
}
void BrowserManager::AddObserver(BrowserManagerObserver* observer) {
observers_.AddObserver(observer);
}
void BrowserManager::RemoveObserver(BrowserManagerObserver* observer) {
observers_.RemoveObserver(observer);
}
void BrowserManager::Start() {
DCHECK_EQ(state_, State::STOPPED);
DCHECK(!lacros_path_.empty());
// Ensure we're not trying to open a window before the shelf is initialized.
DCHECK(ChromeLauncherController::instance());
state_ = State::CREATING_LOG_FILE;
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()}, base::BindOnce(&CreateLogFile),
base::BindOnce(&BrowserManager::StartWithLogFile,
weak_factory_.GetWeakPtr()));
}
void BrowserManager::StartWithLogFile(base::ScopedFD logfd) {
DCHECK_EQ(state_, State::CREATING_LOG_FILE);
std::string chrome_path = lacros_path_.MaybeAsASCII() + "/chrome";
LOG(WARNING) << "Launching lacros-chrome at " << chrome_path;
base::LaunchOptions options;
options.environment["EGL_PLATFORM"] = "surfaceless";
options.environment["XDG_RUNTIME_DIR"] = GetXdgRuntimeDir();
std::string api_key;
if (google_apis::HasAPIKeyConfigured())
api_key = google_apis::GetAPIKey();
else
api_key = google_apis::GetNonStableAPIKey();
options.environment["GOOGLE_API_KEY"] = api_key;
options.environment["GOOGLE_DEFAULT_CLIENT_ID"] =
google_apis::GetOAuth2ClientID(google_apis::CLIENT_MAIN);
options.environment["GOOGLE_DEFAULT_CLIENT_SECRET"] =
google_apis::GetOAuth2ClientSecret(google_apis::CLIENT_MAIN);
// This sets the channel for Lacros.
options.environment["CHROME_VERSION_EXTRA"] = "dev";
options.kill_on_parent_death = true;
// Paths are UTF-8 safe on Chrome OS.
std::string user_data_dir = browser_util::GetUserDataDir().AsUTF8Unsafe();
std::string crash_dir =
browser_util::GetUserDataDir().Append("crash_dumps").AsUTF8Unsafe();
// Pass the locale via command line instead of via LacrosInitParams because
// the Lacros browser process needs it early in startup, before zygote fork.
std::string locale = g_browser_process->GetApplicationLocale();
// Static configuration should be enabled from Lacros rather than Ash. This
// vector should only be used for dynamic configuration.
// TODO(https://crbug.com/1145713): Remove existing static configuration.
std::vector<std::string> argv = {chrome_path,
"--ozone-platform=wayland",
"--user-data-dir=" + user_data_dir,
"--enable-gpu-rasterization",
"--enable-oop-rasterization",
"--lang=" + locale,
"--enable-crashpad",
"--enable-webgl-image-chromium",
"--breakpad-dump-location=" + crash_dir};
// CrAS is the default audio server in Chrome OS.
if (base::SysInfo::IsRunningOnChromeOS())
argv.push_back("--use-cras");
std::string additional_flags =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
chromeos::switches::kLacrosChromeAdditionalArgs);
std::vector<std::string> delimited_flags = base::SplitStringUsingSubstr(
additional_flags, "####", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (const std::string& flag : delimited_flags) {
argv.push_back(flag);
}
// If logfd is valid, enable logging and redirect stdout/stderr to logfd.
if (logfd.is_valid()) {
// The next flag will make chrome log only via stderr. See
// DetermineLoggingDestination in logging_chrome.cc.
argv.push_back("--enable-logging=stderr");
// These options will assign stdout/stderr fds to logfd in the fd table of
// the new process.
options.fds_to_remap.push_back(std::make_pair(logfd.get(), STDOUT_FILENO));
options.fds_to_remap.push_back(std::make_pair(logfd.get(), STDERR_FILENO));
}
base::ScopedFD startup_fd =
browser_util::CreateStartupData(environment_provider_.get());
if (startup_fd.is_valid()) {
// Hardcoded to use FD 3 to make the ash-chrome's behavior more predictable.
// Lacros-chrome should not depend on the hardcoded value though. Instead
// it should take a look at the value passed via the command line flag.
constexpr int kStartupDataFD = 3;
argv.push_back(base::StringPrintf(
"--%s=%d", chromeos::switches::kCrosStartupDataFD, kStartupDataFD));
options.fds_to_remap.emplace_back(startup_fd.get(), kStartupDataFD);
}
// Set up Mojo channel.
base::CommandLine command_line(argv);
LOG(WARNING) << "Launching lacros with command: "
<< command_line.GetCommandLineString();
mojo::PlatformChannel channel;
channel.PrepareToPassRemoteEndpoint(&options, &command_line);
// TODO(crbug.com/1124490): Support multiple mojo connections from lacros.
lacros_chrome_service_ = browser_util::SendMojoInvitationToLacrosChrome(
environment_provider_.get(), channel.TakeLocalEndpoint(),
base::BindOnce(&BrowserManager::OnMojoDisconnected,
weak_factory_.GetWeakPtr()),
base::BindOnce(&BrowserManager::OnAshChromeServiceReceiverReceived,
weak_factory_.GetWeakPtr()));
lacros_chrome_service_.QueryVersion(
base::BindOnce(&BrowserManager::OnLacrosChromeServiceVersionReady,
weak_factory_.GetWeakPtr()));
// Create the lacros-chrome subprocess.
base::RecordAction(base::UserMetricsAction("Lacros.Launch"));
lacros_launch_time_ = base::TimeTicks::Now();
// If lacros_process_ already exists, because it does not call waitpid(2),
// the process will never be collected.
lacros_process_ = base::LaunchProcess(command_line, options);
if (!lacros_process_.IsValid()) {
LOG(ERROR) << "Failed to launch lacros-chrome";
state_ = State::STOPPED;
return;
}
state_ = State::STARTING;
LOG(WARNING) << "Launched lacros-chrome with pid " << lacros_process_.Pid();
channel.RemoteProcessLaunchAttempted();
}
void BrowserManager::OnAshChromeServiceReceiverReceived(
mojo::PendingReceiver<crosapi::mojom::AshChromeService> pending_receiver) {
DCHECK_EQ(state_, State::STARTING);
ash_chrome_service_ =
std::make_unique<AshChromeServiceImpl>(std::move(pending_receiver));
state_ = State::RUNNING;
base::UmaHistogramMediumTimes("ChromeOS.Lacros.StartTime",
base::TimeTicks::Now() - lacros_launch_time_);
// Set the launch-on-login pref every time lacros-chrome successfully starts,
// instead of once during ash-chrome shutdown, so we have the right value
// even if ash-chrome crashes.
SetLaunchOnLoginPref(true);
LOG(WARNING) << "Connection to lacros-chrome is established.";
}
void BrowserManager::OnMojoDisconnected() {
DCHECK(state_ == State::STARTING || state_ == State::RUNNING);
LOG(WARNING)
<< "Mojo to lacros-chrome is disconnected. Terminating lacros-chrome";
state_ = State::TERMINATING;
lacros_chrome_service_.reset();
ash_chrome_service_ = nullptr;
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::WithBaseSyncPrimitives()},
base::BindOnce(&TerminateLacrosChrome, std::move(lacros_process_)),
base::BindOnce(&BrowserManager::OnLacrosChromeTerminated,
weak_factory_.GetWeakPtr()));
NotifyMojoDisconnected();
}
void BrowserManager::OnLacrosChromeTerminated() {
DCHECK_EQ(state_, State::TERMINATING);
LOG(WARNING) << "Lacros-chrome is terminated";
state_ = State::STOPPED;
// TODO(https://crbug.com/1109366): Restart lacros-chrome if it exits
// abnormally (e.g. crashes). For now, assume the user meant to close it.
SetLaunchOnLoginPref(false);
}
void BrowserManager::OnSessionStateChanged() {
DCHECK_EQ(state_, State::NOT_INITIALIZED);
// Wait for session to become active.
auto* session_manager = session_manager::SessionManager::Get();
if (session_manager->session_state() !=
session_manager::SessionState::ACTIVE) {
LOG(WARNING)
<< "Session not yet active. Lacros-chrome will not be launched yet";
return;
}
// Ensure this isn't run multiple times.
session_manager::SessionManager::Get()->RemoveObserver(this);
// May be null in tests.
if (!component_manager_)
return;
DCHECK(!browser_loader_);
browser_loader_ = std::make_unique<BrowserLoader>(component_manager_);
// Must be checked after user session start because it depends on user type.
if (browser_util::IsLacrosEnabled()) {
state_ = State::LOADING;
browser_loader_->Load(base::BindOnce(&BrowserManager::OnLoadComplete,
weak_factory_.GetWeakPtr()));
} else {
state_ = State::UNAVAILABLE;
browser_loader_->Unload();
}
}
void BrowserManager::OnLoadComplete(const base::FilePath& path) {
DCHECK_EQ(state_, State::LOADING);
lacros_path_ = path;
state_ = path.empty() ? State::UNAVAILABLE : State::STOPPED;
if (load_complete_callback_) {
const bool success = !path.empty();
std::move(load_complete_callback_).Run(success);
}
if (state_ == State::STOPPED && GetLaunchOnLoginPref()) {
Start();
}
}
void BrowserManager::NotifyMojoDisconnected() {
for (auto& observer : observers_)
observer.OnMojoDisconnected();
}
void BrowserManager::OnLacrosChromeServiceVersionReady(uint32_t version) {
lacros_chrome_service_version_ = version;
}
} // namespace crosapi