// 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 "fuchsia_web/runners/cast/test/cast_runner_launcher.h"
#include <chromium/cast/cpp/fidl.h>
#include <fuchsia/buildinfo/cpp/fidl.h>
#include <fuchsia/camera3/cpp/fidl.h>
#include <fuchsia/component/decl/cpp/fidl.h>
#include <fuchsia/fonts/cpp/fidl.h>
#include <fuchsia/intl/cpp/fidl.h>
#include <fuchsia/kernel/cpp/fidl.h>
#include <fuchsia/legacymetrics/cpp/fidl.h>
#include <fuchsia/logger/cpp/fidl.h>
#include <fuchsia/media/cpp/fidl.h>
#include <fuchsia/memorypressure/cpp/fidl.h>
#include <fuchsia/net/interfaces/cpp/fidl.h>
#include <fuchsia/settings/cpp/fidl.h>
#include <fuchsia/sysmem/cpp/fidl.h>
#include <fuchsia/tracing/provider/cpp/fidl.h>
#include <fuchsia/ui/composition/cpp/fidl.h>
#include <fuchsia/web/cpp/fidl.h>
#include <lib/vfs/cpp/pseudo_dir.h>
#include <lib/vfs/cpp/remote_dir.h>
#include <memory>
#include <utility>
#include "base/command_line.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/process_context.h"
#include "base/run_loop.h"
#include "fuchsia_web/common/test/test_realm_support.h"
#include "media/fuchsia/audio/fake_audio_device_enumerator_local_component.h"
using ::component_testing::ChildRef;
using ::component_testing::Directory;
using ::component_testing::DirectoryContents;
using ::component_testing::FrameworkRef;
using ::component_testing::ParentRef;
using ::component_testing::Protocol;
using ::component_testing::RealmBuilder;
using ::component_testing::Route;
using ::component_testing::Storage;
namespace test {
namespace {
// Name and path under which components must define a writable directory
// capability, for DynamicComponentHost to dynamically offer per-component
// service directories from.
constexpr char kDynamicComponentCapabilitiesName[] =
"for_dynamic_component_host";
constexpr char kDynamicComponentCapabilitiesPath[] =
"/for_dynamic_component_host";
class TestProxyLocalComponent : public component_testing::LocalComponentImpl {
public:
TestProxyLocalComponent() = default;
// LocalComponentImpl implementation.
void OnStart() override {
// Create a new Directory capability serving the contents of the
// "for_dynamic_component_host" sub-directory of the calling process'
// outgoing directory.
vfs::PseudoDir* capability_dir =
base::ComponentContextForProcess()->outgoing()->GetOrCreateDirectory(
kDynamicComponentCapabilitiesName);
fidl::InterfaceHandle<fuchsia::io::Directory> services;
zx_status_t status =
capability_dir->Serve(fuchsia::io::OpenFlags::RIGHT_READABLE |
fuchsia::io::OpenFlags::RIGHT_WRITABLE |
fuchsia::io::OpenFlags::DIRECTORY,
services.NewRequest().TakeChannel());
ZX_CHECK(status == ZX_OK, status) << "Serve()";
// Bind that Directory capability under the same path of this virtual
// component's outgoing-directory, so that attempts to open files under
// it will be routed back to the calling process' outgoing directory.
status = outgoing()->root_dir()->AddEntry(
kDynamicComponentCapabilitiesName,
std::make_unique<vfs::RemoteDir>(std::move(services)));
ZX_CHECK(status == ZX_OK, status) << "AddEntry";
}
};
} // namespace
CastRunnerLauncher::CastRunnerLauncher(CastRunnerFeatures runner_features) {
auto realm_builder = RealmBuilder::Create();
static constexpr char kCastRunnerComponentName[] = "cast_runner";
realm_builder.AddChild(kCastRunnerComponentName, "#meta/cast_runner.cm");
base::CommandLine command_line = CommandLineFromFeatures(runner_features);
static constexpr char const* kSwitchesToCopy[] = {"ozone-platform"};
command_line.CopySwitchesFrom(*base::CommandLine::ForCurrentProcess(),
kSwitchesToCopy);
AppendCommandLineArguments(realm_builder, kCastRunnerComponentName,
command_line);
// Register the fake fuchsia.feedback service component; plumbing its
// protocols to cast_runner.
FakeFeedbackService::RouteToChild(realm_builder, kCastRunnerComponentName);
AddSyslogRoutesFromParent(realm_builder, kCastRunnerComponentName);
// Run an isolated font service and route it to cast_runner.
AddFontService(realm_builder, kCastRunnerComponentName);
// Run the test-ui-stack and route it to cast_runner.
AddTestUiStack(realm_builder, kCastRunnerComponentName);
realm_builder.AddRoute(Route{
.capabilities =
{
// The chromium test realm offers the system-wide config-data dir
// to test components. Route the cast_runner sub-directory of this
// to the launched cast_runner component.
Directory{.name = "config-data", .subdir = "cast_runner"},
// And route the web_engine sub-directory as required by
// WebInstanceHost.
Directory{.name = "config-data",
.as = "config-data-for-web-instance",
.subdir = "web_engine"},
Directory{.name = "root-ssl-certificates"},
Directory{.name = "tzdata-icu"},
// fuchsia.web/Context required and recommended protocols.
Protocol{fuchsia::buildinfo::Provider::Name_},
Protocol{"fuchsia.device.NameProvider"},
// "fuchsia.fonts.Provider" is provided above.
Protocol{"fuchsia.hwinfo.Product"},
Protocol{fuchsia::intl::PropertyProvider::Name_},
Protocol{fuchsia::kernel::VmexResource::Name_},
Protocol{"fuchsia.logger.LogSink"},
Protocol{fuchsia::media::ProfileProvider::Name_},
Protocol{fuchsia::memorypressure::Provider::Name_},
Protocol{"fuchsia.process.Launcher"},
Protocol{"fuchsia.sysmem.Allocator"},
Protocol{"fuchsia.sysmem2.Allocator"},
// CastRunner sets ContextFeatureFlags::NETWORK by default.
Protocol{fuchsia::net::interfaces::State::Name_},
Protocol{"fuchsia.net.name.Lookup"},
Protocol{"fuchsia.posix.socket.Provider"},
Storage{.name = "cache", .path = "/cache"},
},
.source = ParentRef(),
.targets = {ChildRef{kCastRunnerComponentName}}});
// Provide a fake Cast "agent", providing some necessary services.
static constexpr char kFakeCastAgentName[] = "fake-cast-agent";
auto fake_cast_agent = std::make_unique<FakeCastAgent>();
fake_cast_agent_ = fake_cast_agent.get();
realm_builder.AddLocalChild(
kFakeCastAgentName,
[fake_cast_agent = std::move(fake_cast_agent)]() mutable {
return std::move(fake_cast_agent);
});
realm_builder.AddRoute(
Route{.capabilities =
{
Protocol{chromium::cast::ApplicationConfigManager::Name_},
Protocol{chromium::cast::CorsExemptHeaderProvider::Name_},
Protocol{fuchsia::camera3::DeviceWatcher::Name_},
Protocol{fuchsia::legacymetrics::MetricsRecorder::Name_},
Protocol{fuchsia::media::Audio::Name_},
},
.source = ChildRef{kFakeCastAgentName},
.targets = {ChildRef{kCastRunnerComponentName}}});
if (!(runner_features & kCastRunnerFeaturesHeadless)) {
// CastRunner sets ThemeType::DEFAULT when not headless.
AddRouteFromParent(realm_builder, kCastRunnerComponentName,
fuchsia::settings::Display::Name_);
}
if (runner_features & kCastRunnerFeaturesVulkan) {
AddVulkanRoutesFromParent(realm_builder, kCastRunnerComponentName);
}
// Either route the fake AudioDeviceEnumerator or the system one.
if (runner_features & kCastRunnerFeaturesFakeAudioDeviceEnumerator) {
static constexpr char kAudioDeviceEnumerator[] =
"fake_audio_device_enumerator";
realm_builder.AddLocalChild(kAudioDeviceEnumerator, []() {
return std::make_unique<media::FakeAudioDeviceEnumeratorLocalComponent>();
});
realm_builder.AddRoute(
Route{.capabilities = {Protocol{
fuchsia::media::AudioDeviceEnumerator::Name_}},
.source = ChildRef{kAudioDeviceEnumerator},
.targets = {ChildRef{kCastRunnerComponentName}}});
} else {
AddRouteFromParent(realm_builder, kCastRunnerComponentName,
fuchsia::media::AudioDeviceEnumerator::Name_);
}
// Always offer tracing to CastRunner to suppress log spam. See its CML file.
AddRouteFromParent(realm_builder, kCastRunnerComponentName,
"fuchsia.tracing.provider.Registry");
// TODO(crbug.com/42050521) Remove once not needed to avoid log spam.
AddRouteFromParent(realm_builder, kCastRunnerComponentName,
"fuchsia.tracing.perfetto.ProducerConnector");
// Route capabilities from the cast_runner back up to the test.
realm_builder.AddRoute(
Route{.capabilities = {Protocol{chromium::cast::DataReset::Name_},
Protocol{fuchsia::web::FrameHost::Name_},
Protocol{fuchsia::web::Debug::Name_}},
.source = ChildRef{kCastRunnerComponentName},
.targets = {ParentRef()}});
// Define a pseudo-component to bridge from test-scoped outgoing directory
// into the RealmBuilder scope.
static constexpr char kTestProxyName[] = "test_proxy";
realm_builder.AddLocalChild(kTestProxyName,
std::make_unique<TestProxyLocalComponent>);
// Define an environment that uses the exposed Resolver and Runner, and a
// child component collection that uses that environment, in the test proxy
// component.
static constexpr char kCastResolverName[] = "cast-resolver";
static constexpr char kCastRunnerName[] = "cast-runner";
{
auto test_proxy_decl = realm_builder.GetComponentDecl(kTestProxyName);
static constexpr char kEnvironmentName[] = "cast-test-environment";
static constexpr char kCastUrlScheme[] = "cast";
fuchsia::component::decl::ResolverRegistration resolver_decl;
resolver_decl.set_resolver(kCastResolverName);
resolver_decl.set_source(fuchsia::component::decl::Ref::WithParent({}));
resolver_decl.set_scheme(kCastUrlScheme);
fuchsia::component::decl::RunnerRegistration runner_decl;
runner_decl.set_source_name(kCastRunnerName);
runner_decl.set_source(fuchsia::component::decl::Ref::WithParent({}));
runner_decl.set_target_name(kCastRunnerName);
fuchsia::component::decl::Environment environment_decl;
environment_decl.set_name(kEnvironmentName);
environment_decl.set_extends(
fuchsia::component::decl::EnvironmentExtends::NONE);
environment_decl.set_stop_timeout_ms(1000); // Matches cast_runner.cml
environment_decl.mutable_resolvers()->emplace_back(
std::move(resolver_decl));
environment_decl.mutable_runners()->emplace_back(std::move(runner_decl));
test_proxy_decl.mutable_environments()->emplace_back(
std::move(environment_decl));
fuchsia::component::decl::Collection collection_decl;
collection_decl.set_name(kTestCollectionName);
collection_decl.set_environment(kEnvironmentName);
collection_decl.set_durability(
fuchsia::component::decl::Durability::TRANSIENT);
collection_decl.set_allowed_offers(
fuchsia::component::decl::AllowedOffers::STATIC_AND_DYNAMIC);
test_proxy_decl.mutable_collections()->emplace_back(
std::move(collection_decl));
test_proxy_decl.mutable_capabilities()->emplace_back(
fuchsia::component::decl::Capability::WithDirectory(std::move(
fuchsia::component::decl::Directory()
.set_name(kDynamicComponentCapabilitiesName)
.set_rights(fuchsia::io::RW_STAR_DIR)
.set_source_path(kDynamicComponentCapabilitiesPath))));
test_proxy_decl.mutable_exposes()->emplace_back(
fuchsia::component::decl::Expose::WithProtocol(std::move(
fuchsia::component::decl::ExposeProtocol()
.set_source(fuchsia::component::decl::Ref::WithFramework({}))
.set_source_name(fuchsia::component::Realm::Name_)
.set_target(fuchsia::component::decl::Ref::WithParent({}))
.set_target_name(fuchsia::component::Realm::Name_))));
realm_builder.ReplaceComponentDecl(kTestProxyName,
std::move(test_proxy_decl));
}
// Expose the CastRunner's Realm to the test Realm root, for it to expose
// for use by integration tests (see below).
{
auto runner_decl = realm_builder.GetComponentDecl(kCastRunnerComponentName);
runner_decl.mutable_exposes()->emplace_back(
fuchsia::component::decl::Expose::WithProtocol(std::move(
fuchsia::component::decl::ExposeProtocol()
.set_source(fuchsia::component::decl::Ref::WithFramework({}))
.set_source_name(fuchsia::component::Realm::Name_)
.set_target(fuchsia::component::decl::Ref::WithParent({}))
.set_target_name(fuchsia::component::Realm::Name_))));
realm_builder.ReplaceComponentDecl(kCastRunnerComponentName,
std::move(runner_decl));
}
// Offer the test-proxy the Cast Resolver and Runner capabilities, and
// expose its framework-provided Realm protocol out to the test.
{
auto realm_decl = realm_builder.GetRealmDecl();
realm_decl.mutable_exposes()->emplace_back(
fuchsia::component::decl::Expose::WithProtocol(std::move(
fuchsia::component::decl::ExposeProtocol()
.set_source(fuchsia::component::decl::Ref::WithChild(
fuchsia::component::decl::ChildRef{.name = kTestProxyName}))
.set_source_name(fuchsia::component::Realm::Name_)
.set_target(fuchsia::component::decl::Ref::WithParent({}))
.set_target_name(fuchsia::component::Realm::Name_))));
realm_decl.mutable_offers()->emplace_back(
fuchsia::component::decl::Offer::WithResolver(std::move(
fuchsia::component::decl::OfferResolver()
.set_source(fuchsia::component::decl::Ref::WithChild(
fuchsia::component::decl::ChildRef{
.name = kCastRunnerComponentName}))
.set_source_name(kCastResolverName)
.set_target(fuchsia::component::decl::Ref::WithChild(
fuchsia::component::decl::ChildRef{.name = kTestProxyName}))
.set_target_name(kCastResolverName))));
realm_decl.mutable_offers()->emplace_back(
fuchsia::component::decl::Offer::WithRunner(std::move(
fuchsia::component::decl::OfferRunner()
.set_source(fuchsia::component::decl::Ref::WithChild(
fuchsia::component::decl::ChildRef{
.name = kCastRunnerComponentName}))
.set_source_name(kCastRunnerName)
.set_target(fuchsia::component::decl::Ref::WithChild(
fuchsia::component::decl::ChildRef{.name = kTestProxyName}))
.set_target_name(kCastRunnerName))));
// Expose the CastRunner's Realm via the root component, as
// "fuchsia.component.Realm:runner", to allow tests to e.g.
// manipulate the child components in the `web_instances` collection.
realm_decl.mutable_exposes()->emplace_back(
fuchsia::component::decl::Expose::WithProtocol(std::move(
fuchsia::component::decl::ExposeProtocol()
.set_source(fuchsia::component::decl::Ref::WithChild(
fuchsia::component::decl::ChildRef{
.name = kCastRunnerComponentName}))
.set_source_name(fuchsia::component::Realm::Name_)
.set_target(fuchsia::component::decl::Ref::WithParent({}))
.set_target_name(kCastRunnerRealmProtocol))));
realm_builder.ReplaceRealmDecl(std::move(realm_decl));
}
// Create the test realm and connect to the root component's exposed services,
// for use by tests.
realm_root_ = realm_builder.Build();
exposed_services_ = std::make_unique<sys::ServiceDirectory>(
realm_root_->component().CloneExposedDir());
}
CastRunnerLauncher::~CastRunnerLauncher() {
if (realm_root_.has_value()) {
base::RunLoop run_loop;
realm_root_.value().Teardown(
[quit = run_loop.QuitClosure()](auto result) { quit.Run(); });
run_loop.Run();
}
}
} // namespace test