blob: 72fc2627f40cdf948f81e610573373f966ef9a3d [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include <memory>
#include <tuple>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/location.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/commands/web_app_command.h"
#include "chrome/browser/web_applications/locks/lock.h"
#include "chrome/browser/web_applications/locks/web_app_lock_manager.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_install_manager.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_contents/web_app_url_loader.h"
#include "content/public/browser/web_contents.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace web_app {
namespace {
base::Value::Dict CreateCommandMetadata(const WebAppCommand& command) {
base::Value::Dict dict;
dict.Set("name", command.name());
dict.Set("id", command.id());
if (command.scheduled_location().has_value()) {
dict.Set("scheduled_location",
command.scheduled_location().value().ToString());
}
return dict;
}
base::Value::Dict CreateLogValue(const WebAppCommand& command,
absl::optional<CommandResult> result) {
base::Value::Dict dict = CreateCommandMetadata(command);
base::Value debug_value = command.ToDebugValue();
bool is_empty_dict = debug_value.is_dict() && debug_value.GetDict().empty();
if (!debug_value.is_none() && !is_empty_dict) {
dict.Set("value", std::move(debug_value));
}
if (result) {
switch (result.value()) {
case CommandResult::kSuccess:
dict.Set("result", "kSuccess");
break;
case CommandResult::kFailure:
dict.Set("result", "kFailure");
break;
case CommandResult::kShutdown:
dict.Set("result", "kShutdown");
break;
}
}
return dict;
}
} // namespace
WebAppCommandManager::WebAppCommandManager(Profile* profile,
WebAppProvider* provider)
: profile_(profile),
provider_(provider),
url_loader_(std::make_unique<WebAppUrlLoader>()),
lock_manager_(std::make_unique<WebAppLockManager>(*provider_)) {}
WebAppCommandManager::~WebAppCommandManager() {
// Make sure that unittests & browsertests correctly shut down the manager.
// This ensures that all tests also cover shutdown.
DCHECK(is_in_shutdown_);
}
void WebAppCommandManager::Start() {
started_ = true;
std::vector<std::unique_ptr<WebAppCommand>> to_schedule;
std::swap(commands_waiting_for_start_, to_schedule);
for (auto& command : to_schedule) {
ScheduleCommand(std::move(command));
}
}
void WebAppCommandManager::ScheduleCommand(
std::unique_ptr<WebAppCommand> command,
const base::Location& location) {
DCHECK(command);
command->SetScheduledLocation(location);
DVLOG(2) << "Scheduling command: " << CreateCommandMetadata(*command);
if (!started_) {
commands_waiting_for_start_.push_back(std::move(command));
return;
}
if (is_in_shutdown_) {
AddValueToLog(
base::Value(CreateLogValue(*command, CommandResult::kShutdown)));
return;
}
DCHECK(!base::Contains(commands_, command->id()));
auto command_id = command->id();
auto command_it = commands_.try_emplace(command_id, std::move(command)).first;
command_it->second->RequestLock(
this, lock_manager_.get(),
base::BindOnce(&WebAppCommandManager::OnLockAcquired,
weak_ptr_factory_.GetWeakPtr(), command_id),
location);
}
void WebAppCommandManager::OnLockAcquired(WebAppCommand::Id command_id,
base::OnceClosure start_command) {
if (is_in_shutdown_)
return;
auto command_it = commands_.find(command_id);
DCHECK(command_it != commands_.end());
// Start is called in a new task to avoid re-entry issues with started tasks
// calling back into Enqueue/Destroy. This can especially be an issue if
// this task is being run in response to a call to
// NotifySyncSourceRemoved.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&WebAppCommandManager::StartCommandOrPrepareForLoad,
weak_ptr_factory_.GetWeakPtr(), command_it->second.get(),
std::move(start_command)));
}
void WebAppCommandManager::StartCommandOrPrepareForLoad(
WebAppCommand* command,
base::OnceClosure start_command) {
if (is_in_shutdown_)
return;
#if DCHECK_IS_ON()
DCHECK(command);
auto command_it = commands_.find(command->id());
DCHECK(command_it != commands_.end());
#endif
if (command->lock_description().IncludesSharedWebContents()) {
CHECK(shared_web_contents_);
url_loader_->PrepareForLoad(
// web_contents is created by `WebAppLockManager` when lock is granted,
// this grabs the same web_contents.
shared_web_contents_.get(),
base::BindOnce(&WebAppCommandManager::OnAboutBlankLoadedForCommandStart,
weak_ptr_factory_.GetWeakPtr(), command,
std::move(start_command)));
return;
}
DVLOG(2) << "Starting command: " << CreateCommandMetadata(*command);
std::move(start_command).Run();
}
void WebAppCommandManager::OnAboutBlankLoadedForCommandStart(
WebAppCommand* command,
base::OnceClosure start_command,
WebAppUrlLoader::Result result) {
if (is_in_shutdown_) {
return;
}
DCHECK(shared_web_contents_);
// about:blank must always be loaded.
DCHECK_EQ(WebAppUrlLoader::Result::kUrlLoaded, result);
if (result != WebAppUrlLoader::Result::kUrlLoaded) {
base::Value::Dict url_loader_error;
url_loader_error.Set("WebAppUrlLoader::Result",
ConvertUrlLoaderResultToString(result));
if (command->lock_description().app_ids().size() == 1) {
url_loader_error.Set("task.app_id_to_expect",
*command->lock_description().app_ids().begin());
}
url_loader_error.Set("!stage", "OnWebContentsReady");
provider_->install_manager().TakeCommandErrorLog(
PassKey(), std::move(url_loader_error));
}
DVLOG(2) << "Starting command: " << CreateCommandMetadata(*command);
std::move(start_command).Run();
}
void WebAppCommandManager::Shutdown() {
// Ignore duplicate shutdowns for unittests.
if (is_in_shutdown_)
return;
is_in_shutdown_ = true;
AddValueToLog(base::Value("Shutdown has begun"));
// Create a copy of commands to call `OnShutdown` because commands can call
// `CallSignalCompletionAndSelfDestruct` during `OnShutdown`, which removes
// the command from the `commands_` map.
std::vector<base::WeakPtr<WebAppCommand>> commands_to_shutdown;
for (const auto& [id, command] : commands_) {
DCHECK_CALLED_ON_VALID_SEQUENCE(command->command_sequence_checker_);
if (command->IsStarted()) {
commands_to_shutdown.push_back(command->AsWeakPtr());
}
}
for (const auto& command_ptr : commands_to_shutdown) {
if (!command_ptr)
continue;
command_ptr->OnShutdown();
}
commands_.clear();
shared_web_contents_.reset();
}
base::Value WebAppCommandManager::ToDebugValue() {
base::Value::List command_log;
for (const auto& command_value : command_debug_log_) {
command_log.Append(command_value.Clone());
}
base::Value::List queued;
for (const auto& [id, command] : commands_) {
queued.Append(::web_app::CreateLogValue(*command, absl::nullopt));
}
base::Value::Dict state;
state.Set("command_log", std::move(command_log));
state.Set("command_queue", base::Value(std::move(queued)));
return base::Value(std::move(state));
}
void WebAppCommandManager::LogToInstallManager(base::Value::Dict log) {
#if DCHECK_IS_ON()
// This is wrapped with DCHECK_IS_ON() to prevent calling DebugString() in
// production builds.
DVLOG(1) << log.DebugString();
#endif
provider_->install_manager().TakeCommandErrorLog(PassKey(), std::move(log));
}
bool WebAppCommandManager::IsInstallingForWebContents(
const content::WebContents* web_contents) const {
for (const auto& [id, command] : commands_) {
if (command->GetInstallingWebContents() == web_contents) {
return true;
}
}
return false;
}
void WebAppCommandManager::AwaitAllCommandsCompleteForTesting() {
if (commands_.empty())
return;
if (!run_loop_for_testing_)
run_loop_for_testing_ = std::make_unique<base::RunLoop>();
run_loop_for_testing_->Run();
run_loop_for_testing_.reset();
}
void WebAppCommandManager::SetUrlLoaderForTesting(
std::unique_ptr<WebAppUrlLoader> url_loader) {
url_loader_ = std::move(url_loader);
}
void WebAppCommandManager::OnCommandComplete(
WebAppCommand* running_command,
CommandResult result,
base::OnceClosure completion_callback) {
DCHECK(running_command);
AddValueToLog(base::Value(CreateLogValue(*running_command, result)));
auto command_it = commands_.find(running_command->id());
DCHECK(command_it != commands_.end());
commands_.erase(command_it);
if (shared_web_contents_) {
bool lock_free = lock_manager_->IsSharedWebContentsLockFree();
if (lock_free) {
AddValueToLog(base::Value("Destroying the shared web contents."));
shared_web_contents_.reset();
}
}
std::move(completion_callback).Run();
if (commands_.empty() && run_loop_for_testing_)
run_loop_for_testing_->Quit();
}
void WebAppCommandManager::AddValueToLog(base::Value value) {
DCHECK(!value.is_none());
#if DCHECK_IS_ON()
// This is wrapped with DCHECK_IS_ON() to prevent calling DebugString() in
// production builds.
DVLOG(1) << value.DebugString();
#endif
static constexpr const int kMaxLogLength = 20;
command_debug_log_.push_front(std::move(value));
if (command_debug_log_.size() > kMaxLogLength)
command_debug_log_.resize(kMaxLogLength);
}
content::WebContents* WebAppCommandManager::EnsureWebContentsCreated(
base::PassKey<WebAppLockManager>) {
return EnsureWebContentsCreated();
}
content::WebContents* WebAppCommandManager::EnsureWebContentsCreated() {
DCHECK(profile_);
if (!shared_web_contents_)
shared_web_contents_ = content::WebContents::Create(
content::WebContents::CreateParams(profile_));
web_app::CreateWebAppInstallTabHelpers(shared_web_contents_.get());
return shared_web_contents_.get();
}
} // namespace web_app