// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/ash/components/dbus/featured/featured_client.h"
#include <memory>
#include <string>
#include <string_view>
#include "base/check_is_test.h"
#include "base/files/dir_reader_posix.h"
#include "base/files/file_path.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "base/task/thread_pool.h"
#include "chromeos/ash/components/dbus/featured/fake_featured_client.h"
#include "chromeos/ash/components/dbus/featured/featured.pb.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "third_party/cros_system_api/constants/featured.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
namespace ash::featured {
namespace {
FeaturedClient* g_instance = nullptr;
struct FileWatchOptions {
FeaturedClient::ListenForTrialCallback listen_callback;
const base::FilePath expected_dir;
};
void RecordEarlyBootTrialInUMA(const std::string& trial_name,
const std::string& group_name) {
base::FieldTrial* trial =
base::FieldTrialList::CreateFieldTrial(trial_name, group_name);
// This records the trial in UMA.
trial->Activate();
}
// Assumes |filename| is valid (that its directory name is correct).
bool RecordEarlyBootTrialInChrome(
FeaturedClient::ListenForTrialCallback listen_callback,
const base::FilePath& filename) {
base::FieldTrial::ActiveGroup active_group;
if (!FeaturedClient::ParseTrialFilename(filename, active_group)) {
return false;
}
listen_callback.Run(active_group.trial_name, active_group.group_name);
return true;
}
void RecordEarlyBootTrialAfterChromeStartup(
const FileWatchOptions& opts,
const base::FilePathWatcher::ChangeInfo& change_info,
const base::FilePath& path,
bool error) {
if (error || path.DirName() != opts.expected_dir) {
// TODO(b/296394808): Add UMA metric if we enter this code path since it
// is not expected.
return;
}
if (change_info.file_path_type !=
base::FilePathWatcher::FilePathType::kFile ||
change_info.change_type != base::FilePathWatcher::ChangeType::kCreated) {
// Only record field trial files that were just created. We do not want to
// record a field trial on any of the other change options like
// base::FilePathWatcher::ChangeType::kModified or
// base::FilePathWatcher::ChangeType::kDeleted.
return;
}
// TODO(b/296394808): Add UMA metric if unable to record trial due to parse
// error.
RecordEarlyBootTrialInChrome(opts.listen_callback, path);
}
void ListenForActiveEarlyBootTrials(base::FilePathWatcher* watcher,
const FileWatchOptions& opts) {
base::FilePathWatcher::WatchOptions options = {
// Watches for changes in a directory.
.type = base::FilePathWatcher::Type::kRecursive,
// Reports the path of modified files in the directory.
.report_modified_path = true};
watcher->WatchWithChangeInfo(
opts.expected_dir, options,
base::BindRepeating(&RecordEarlyBootTrialAfterChromeStartup, opts));
}
void ReadTrialsActivatedBeforeChromeStartup(const FileWatchOptions& opts) {
base::DirReaderPosix reader(opts.expected_dir.value().c_str());
if (!reader.IsValid()) {
// TODO(b/296394808): Add UMA metric if we are unable to enumerate trials
// activated before Chrome startup.
return;
}
while (reader.Next()) {
if (std::string(reader.name()) == "." ||
std::string(reader.name()) == "..") {
continue;
}
// TODO(b/296394808): Add UMA metric if unable to record trial due to
// parse error.
RecordEarlyBootTrialInChrome(opts.listen_callback,
base::FilePath(reader.name()));
}
}
// We need to delete the FilePathWatcher instance via a posted task since we
// call FilePathWatche::Watch() on a posted task. The documentation states the
// instance must be destroyed on the same sequence it watches from.
void DeleteWatcher(std::unique_ptr<base::FilePathWatcher> watcher) {
watcher.reset();
}
// Production implementation of FeaturedClient.
class FeaturedClientImpl : public FeaturedClient {
public:
FeaturedClientImpl() : watcher_(std::make_unique<base::FilePathWatcher>()) {}
FeaturedClientImpl(const FeaturedClient&) = delete;
FeaturedClientImpl operator=(const FeaturedClient&) = delete;
~FeaturedClientImpl() override {
file_listener_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&DeleteWatcher, std::move(watcher_)));
}
void Init(dbus::Bus* const bus) {
InitWithCallback(bus, base::FilePath(feature::kActiveTrialFileDirectory),
base::BindRepeating(&RecordEarlyBootTrialInUMA));
}
void InitForTesting(dbus::Bus* const bus, // IN-TEST
const base::FilePath& expected_dir,
ListenForTrialCallback callback) {
CHECK_IS_TEST();
InitWithCallback(bus, expected_dir, callback);
}
void HandleSeedFetchedResponse(
base::OnceCallback<void(bool success)> callback,
dbus::Response* response) {
if (!response ||
response->GetMessageType() != dbus::Message::MESSAGE_METHOD_RETURN) {
LOG(WARNING) << "Received invalid response for HandleSeedFetched";
std::move(callback).Run(false);
return;
}
std::move(callback).Run(true);
}
void HandleSeedFetched(
const ::featured::SeedDetails& safe_seed,
base::OnceCallback<void(bool success)> callback) override {
dbus::MethodCall method_call(::featured::kFeaturedInterface,
"HandleSeedFetched");
dbus::MessageWriter writer(&method_call);
writer.AppendProtoAsArrayOfBytes(safe_seed);
featured_service_proxy_->CallMethod(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&FeaturedClientImpl::HandleSeedFetchedResponse,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
private:
void InitWithCallback(dbus::Bus* const bus,
const base::FilePath& expected_dir,
ListenForTrialCallback callback) {
featured_service_proxy_ =
bus->GetObjectProxy(::featured::kFeaturedServiceName,
dbus::ObjectPath(::featured::kFeaturedServicePath));
expected_dir_ = expected_dir;
listen_callback_ = callback;
FileWatchOptions opts = {.listen_callback = callback,
.expected_dir = expected_dir};
file_listener_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&ListenForActiveEarlyBootTrials, watcher_.get(), opts));
file_listener_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&ReadTrialsActivatedBeforeChromeStartup, opts));
}
// Callback used when early-boot trial files are written to `expected_dir_`.
FeaturedClient::ListenForTrialCallback listen_callback_;
// Directory where active trial files on platform are written to.
base::FilePath expected_dir_;
// Watches for early-boot trial files written to `expected_dir_`.
std::unique_ptr<base::FilePathWatcher> watcher_;
raw_ptr<dbus::ObjectProxy> featured_service_proxy_ = nullptr;
// Sequence runner that an post tasks that may block.
scoped_refptr<base::SequencedTaskRunner> file_listener_task_runner_ =
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT});
// Note: This should remain the last member so it'll be destroyed and
// invalidate its weak pointers before any other members are destroyed.
base::WeakPtrFactory<FeaturedClientImpl> weak_ptr_factory_{this};
};
} // namespace
FeaturedClient::FeaturedClient() {
DCHECK(!g_instance);
g_instance = this;
}
FeaturedClient::~FeaturedClient() {
DCHECK_EQ(this, g_instance);
g_instance = nullptr;
}
// static
void FeaturedClient::Initialize(dbus::Bus* bus) {
DCHECK(bus);
(new FeaturedClientImpl())->Init(bus);
}
// static
void FeaturedClient::InitializeFake() {
new FakeFeaturedClient();
}
// static
void FeaturedClient::InitializeForTesting(dbus::Bus* bus,
const base::FilePath& expected_dir,
ListenForTrialCallback callback) {
DCHECK(bus);
(new FeaturedClientImpl())
->InitForTesting(bus, expected_dir, callback); // IN-TEST
}
// static
void FeaturedClient::Shutdown() {
DCHECK(g_instance);
delete g_instance;
}
// static
FeaturedClient* FeaturedClient::Get() {
return g_instance;
}
// static
bool FeaturedClient::ParseTrialFilename(
const base::FilePath& path,
base::FieldTrial::ActiveGroup& active_group) {
std::string filename = path.BaseName().value();
std::vector<std::string_view> components =
base::SplitStringPiece(filename, feature::kTrialGroupSeparator,
base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (components.size() != 2) {
LOG(ERROR) << "Active trial filename not of the form TrialName,GroupName: "
<< filename;
return false;
}
std::string trial_name = base::UnescapeURLComponent(
components[0],
base::UnescapeRule::SPACES | base::UnescapeRule::PATH_SEPARATORS |
base::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS);
std::string group_name = base::UnescapeURLComponent(
components[1],
base::UnescapeRule::SPACES | base::UnescapeRule::PATH_SEPARATORS |
base::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS);
active_group.trial_name = trial_name;
active_group.group_name = group_name;
return true;
}
} // namespace ash::featured