blob: e762d88050588b67f06eec16af66c3664039f901 [file] [log] [blame]
// Copyright 2016 The Fuchsia 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 <cmath>
#include <iostream>
#include <memory>
#include <string>
#include <fs/pseudo-file.h>
#include <fuchsia/auth/cpp/fidl.h>
#include <fuchsia/modular/auth/cpp/fidl.h>
#include <fuchsia/modular/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/ui/viewsv1/cpp/fidl.h>
#include <fuchsia/ui/viewsv1token/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/future.h>
#include <lib/component/cpp/startup_context.h>
#include <lib/fidl/cpp/array.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/fidl/cpp/interface_handle.h>
#include <lib/fidl/cpp/interface_request.h>
#include <lib/fidl/cpp/string.h>
#include <lib/fxl/command_line.h>
#include <lib/fxl/logging.h>
#include <lib/fxl/macros.h>
#include <trace-provider/provider.h>
#include "peridot/bin/device_runner/cobalt/cobalt.h"
#include "peridot/bin/device_runner/user_provider_impl.h"
#include "peridot/lib/common/async_holder.h"
#include "peridot/lib/common/names.h"
#include "peridot/lib/common/teardown.h"
#include "peridot/lib/fidl/app_client.h"
#include "peridot/lib/fidl/array_to_string.h"
#include "peridot/lib/fidl/clone.h"
#include "peridot/lib/user_shell_settings/user_shell_settings.h"
#include "peridot/lib/util/filesystem.h"
namespace modular {
namespace {
class Settings {
public:
explicit Settings(const fxl::CommandLine& command_line) {
device_shell.url = command_line.GetOptionValueWithDefault(
"device_shell", "userpicker_device_shell");
story_shell.url =
command_line.GetOptionValueWithDefault("story_shell", "mondrian");
user_runner.url =
command_line.GetOptionValueWithDefault("user_runner", "user_runner");
user_shell.url = command_line.GetOptionValueWithDefault(
"user_shell", "ermine_user_shell");
account_provider.url = command_line.GetOptionValueWithDefault(
"account_provider", "oauth_token_manager");
disable_statistics = command_line.HasOption("disable_statistics");
ignore_monitor = command_line.HasOption("ignore_monitor");
no_minfs = command_line.HasOption("no_minfs");
test = command_line.HasOption("test");
enable_presenter = command_line.HasOption("enable_presenter");
enable_garnet_token_manager =
command_line.HasOption("enable_garnet_token_manager");
ParseShellArgs(
command_line.GetOptionValueWithDefault("device_shell_args", ""),
&device_shell.args);
ParseShellArgs(
command_line.GetOptionValueWithDefault("story_shell_args", ""),
&story_shell.args);
ParseShellArgs(
command_line.GetOptionValueWithDefault("user_runner_args", ""),
&user_runner.args);
ParseShellArgs(
command_line.GetOptionValueWithDefault("user_shell_args", ""),
&user_shell.args);
if (test) {
device_shell.args.push_back("--test");
story_shell.args.push_back("--test");
user_runner.args.push_back("--test");
user_shell.args.push_back("--test");
test_name = FindTestName(user_shell.url, user_shell.args);
disable_statistics = true;
ignore_monitor = true;
no_minfs = true;
}
}
static std::string GetUsage() {
return R"USAGE(device_runner
--device_shell=DEVICE_SHELL
--device_shell_args=SHELL_ARGS
--user_shell=USER_SHELL
--user_shell_args=SHELL_ARGS
--story_shell=STORY_SHELL
--story_shell_args=SHELL_ARGS
--account_provider=ACCOUNT_PROVIDER
--disable_statistics
--ignore_monitor
--no_minfs
--test
--enable_presenter
--enable_garnet_token_manager
DEVICE_NAME: Name which user shell uses to identify this device.
DEVICE_SHELL: URL of the device shell to run.
Defaults to "userpicker_device_shell".
For integration testing use "dev_device_shell".
USER_RUNNER: URL of the user runner to run.
Defaults to "user_runner".
USER_SHELL: URL of the user shell to run.
Defaults to "ermine_user_shell".
For integration testing use "dev_user_shell".
STORY_SHELL: URL of the story shell to run.
Defaults to "mondrian".
For integration testing use "dev_story_shell".
SHELL_ARGS: Comma separated list of arguments. Backslash escapes comma.
ACCOUNT_PROVIDER: URL of the account provider to use.
Defaults to "oauth_token_manager".
For integration tests use "dev_token_manager".)USAGE";
}
fuchsia::modular::AppConfig device_shell;
fuchsia::modular::AppConfig story_shell;
fuchsia::modular::AppConfig user_runner;
fuchsia::modular::AppConfig user_shell;
fuchsia::modular::AppConfig account_provider;
std::string test_name;
bool disable_statistics;
bool ignore_monitor;
bool no_minfs;
bool test;
bool enable_presenter;
bool enable_garnet_token_manager;
private:
void ParseShellArgs(const std::string& value,
fidl::VectorPtr<fidl::StringPtr>* args) {
bool escape = false;
std::string arg;
for (char i : value) {
if (escape) {
arg.push_back(i);
escape = false;
continue;
}
if (i == '\\') {
escape = true;
continue;
}
if (i == ',') {
args->push_back(arg);
arg.clear();
continue;
}
arg.push_back(i);
}
if (!arg.empty()) {
args->push_back(arg);
}
}
// Extract the test name using knowledge of how Modular structures its
// command lines for testing.
static std::string FindTestName(
const fidl::StringPtr& user_shell,
const fidl::VectorPtr<fidl::StringPtr>& user_shell_args) {
const std::string kRootModule = "--root_module";
std::string result = user_shell;
for (const auto& user_shell_arg : *user_shell_args) {
const auto& arg = user_shell_arg.get();
if (arg.substr(0, kRootModule.size()) == kRootModule) {
result = arg.substr(kRootModule.size());
}
}
const auto index = result.find_last_of('/');
if (index == std::string::npos) {
return result;
}
return result.substr(index + 1);
}
FXL_DISALLOW_COPY_AND_ASSIGN(Settings);
};
} // namespace
class DeviceRunnerApp : fuchsia::modular::DeviceShellContext,
fuchsia::modular::auth::AccountProviderContext,
fuchsia::ui::policy::KeyboardCaptureListenerHACK,
modular::UserProviderImpl::Delegate {
public:
explicit DeviceRunnerApp(
const Settings& settings,
std::shared_ptr<component::StartupContext> const context,
std::function<void()> on_shutdown)
: settings_(settings),
user_provider_impl_("UserProviderImpl"),
context_(std::move(context)),
on_shutdown_(std::move(on_shutdown)),
device_shell_context_binding_(this),
account_provider_context_binding_(this) {
if (!context_->has_environment_services()) {
FXL_LOG(ERROR) << "Failed to receive services from the environment.";
exit(1);
}
// TODO(SCN-595): Presentation is now discoverable, so we don't need
// kPresentationService anymore.
service_namespace_.AddService(presentation_state_.bindings.GetHandler(
presentation_state_.presentation.get()),
kPresentationService);
if (settings.ignore_monitor) {
Start();
return;
}
context_->ConnectToEnvironmentService(monitor_.NewRequest());
monitor_.set_error_handler([] {
FXL_LOG(ERROR) << "No device runner monitor found.";
exit(1);
});
monitor_->GetConnectionCount([this](uint32_t count) {
if (count != 1) {
FXL_LOG(ERROR) << "Another device runner is running."
<< " Please use that one, or shut it down first.";
exit(1);
}
Start();
});
}
private:
void InitializePresentation(
fidl::InterfaceHandle<fuchsia::ui::viewsv1token::ViewOwner> view_owner) {
if (settings_.test && !settings_.enable_presenter) {
return;
}
auto presentation_request =
presentation_state_.presentation.is_bound()
? presentation_state_.presentation.Unbind().NewRequest()
: presentation_state_.presentation.NewRequest();
context_->ConnectToEnvironmentService<fuchsia::ui::policy::Presenter>()
->Present(std::move(view_owner), std::move(presentation_request));
AddGlobalKeyboardShortcuts(presentation_state_.presentation);
SetShadowTechnique(presentation_state_.shadow_technique);
}
void StartDeviceShell() {
if (device_shell_running_) {
FXL_DLOG(INFO) << "StartDeviceShell() called when already running";
return;
}
device_shell_app_ =
std::make_unique<AppClient<fuchsia::modular::Lifecycle>>(
context_->launcher().get(), CloneStruct(settings_.device_shell));
device_shell_app_->services().ConnectToService(device_shell_.NewRequest());
fuchsia::ui::viewsv1::ViewProviderPtr device_shell_view_provider;
device_shell_app_->services().ConnectToService(
device_shell_view_provider.NewRequest());
// We still need to pass a request for root view to device shell since
// dev_device_shell (which mimics flutter behavior) blocks until it receives
// the root view request.
fidl::InterfaceHandle<fuchsia::ui::viewsv1token::ViewOwner> root_view;
device_shell_view_provider->CreateView(root_view.NewRequest(), nullptr);
InitializePresentation(std::move(root_view));
// Populate parameters and initialize the device shell.
fuchsia::modular::DeviceShellParams params;
params.presentation = std::move(presentation_state_.presentation);
device_shell_->Initialize(device_shell_context_binding_.NewBinding(),
std::move(params));
device_shell_running_ = true;
}
FuturePtr<> StopDeviceShell() {
if (!device_shell_running_) {
FXL_DLOG(INFO) << "StopDeviceShell() called when already stopped";
return Future<>::CreateCompleted("StopDeviceShell::Completed");
}
auto did_stop = Future<>::Create("StopDeviceShell");
device_shell_app_->Teardown(kBasicTimeout, [did_stop, this] {
FXL_DLOG(INFO) << "- fuchsia::modular::DeviceShell down";
device_shell_running_ = false;
did_stop->Complete();
});
return did_stop;
}
FuturePtr<> StopAccountProvider() {
if (!account_provider_) {
FXL_DLOG(INFO) << "StopAccountProvider() called when already stopped";
return Future<>::CreateCompleted("StopAccountProvider::Completed");
}
auto did_stop = Future<>::Create("StopAccountProvider");
account_provider_->Teardown(kBasicTimeout, [did_stop, this] {
FXL_DLOG(INFO) << "- fuchsia::modular::auth::AccountProvider down";
account_provider_.release();
did_stop->Complete();
});
return did_stop;
}
FuturePtr<> StopTokenManagerFactoryApp() {
if (!token_manager_factory_app_) {
FXL_DLOG(INFO)
<< "StopTokenManagerFactoryApp() called when already stopped";
return Future<>::CreateCompleted("StopTokenManagerFactoryApp::Completed");
}
auto did_stop = Future<>::Create("StopTokenManagerFactoryApp");
token_manager_factory_app_->Teardown(kBasicTimeout, [did_stop, this] {
FXL_DLOG(INFO) << "- fuchsia::auth::TokenManagerFactory down";
token_manager_factory_app_.release();
did_stop->Complete();
});
return did_stop;
}
void Start() {
if (settings_.test) {
// 0. Print test banner.
FXL_LOG(INFO)
<< std::endl
<< std::endl
<< "======================== Starting Test [" << settings_.test_name
<< "]" << std::endl
<< "============================================================"
<< std::endl;
}
// Start the device shell. This is done first so that we can show some UI
// until other things come up.
StartDeviceShell();
// Wait for persistent data to come up.
if (!settings_.no_minfs) {
WaitForMinfs();
}
// Start OAuth Token Manager App.
fuchsia::modular::AppConfig token_manager_config;
token_manager_config.url = settings_.account_provider.url;
if (settings_.enable_garnet_token_manager) {
FXL_DLOG(INFO) << "Initialzing token_manager_factory_app()";
token_manager_factory_app_ =
std::make_unique<AppClient<fuchsia::modular::Lifecycle>>(
context_->launcher().get(), CloneStruct(token_manager_config));
token_manager_factory_app_->services().ConnectToService(
token_manager_factory_.NewRequest());
} else {
token_manager_factory_app_.release();
}
account_provider_ =
std::make_unique<AppClient<fuchsia::modular::auth::AccountProvider>>(
context_->launcher().get(), std::move(token_manager_config),
"/data/modular/ACCOUNT_MANAGER");
account_provider_->SetAppErrorHandler([] {
FXL_CHECK(false) << "Token manager crashed. Stopping device runner.";
});
account_provider_->primary_service()->Initialize(
account_provider_context_binding_.NewBinding());
user_provider_impl_.reset(new UserProviderImpl(
context_, settings_.user_runner, settings_.user_shell,
settings_.story_shell, account_provider_->primary_service().get(),
token_manager_factory_.get(), settings_.enable_garnet_token_manager,
this));
ReportEvent(ModularEvent::BOOTED_TO_DEVICE_RUNNER);
}
// |fuchsia::modular::DeviceShellContext|
void GetUserProvider(
fidl::InterfaceRequest<fuchsia::modular::UserProvider> request) override {
user_provider_impl_->Connect(std::move(request));
}
// |fuchsia::modular::DeviceShellContext|
void Shutdown() override {
// TODO(mesch): Some of these could be done in parallel too.
// fuchsia::modular::UserProvider must go first, but the order after user
// provider is for now rather arbitrary. We terminate device shell last so
// that in tests testing::Teardown() is invoked at the latest possible time.
// Right now it just demonstrates that AppTerminate() works as we like it
// to.
FXL_DLOG(INFO) << "fuchsia::modular::DeviceShellContext::Shutdown()";
if (settings_.test) {
FXL_LOG(INFO)
<< std::endl
<< "============================================================"
<< std::endl
<< "======================== [" << settings_.test_name << "] Done";
}
user_provider_impl_.Teardown(kUserProviderTimeout, [this] {
FXL_DLOG(INFO) << "- fuchsia::modular::UserProvider down";
StopAccountProvider()->Then([this] {
FXL_DLOG(INFO) << "- fuchsia::modular::auth::AccountProvider down";
StopTokenManagerFactoryApp()->Then([this] {
FXL_DLOG(INFO) << "- fuchsia::auth::TokenManagerFactory down";
StopDeviceShell()->Then([this] {
FXL_LOG(INFO) << "Clean Shutdown";
on_shutdown_();
});
});
});
});
}
// |AccountProviderContext|
void GetAuthenticationContext(
fidl::StringPtr account_id,
fidl::InterfaceRequest<fuchsia::modular::auth::AuthenticationContext>
request) override {
// TODO(MI4-1107): DeviceRunner needs to implement AuthenticationContext
// itself, and proxy calls for StartOverlay & StopOverlay to DeviceShell,
// starting it if it's not running yet.
device_shell_->GetAuthenticationContext(account_id, std::move(request));
}
// |UserProviderImpl::Delegate|
void DidLogin() override {
// Continues if `enable_presenter` is set to true during testing, as
// ownership of the Presenter should still be moved to the user shell.
if (settings_.test && !settings_.enable_presenter) {
// TODO(MI4-1117): Integration tests currently expect device shell to
// always be running. So, if we're running under a test, do not shut down
// the device shell after login.
return;
}
// TODO(MI4-1117): See above. The device shell shouldn't be shut down.
if (!settings_.test) {
FXL_DLOG(INFO) << "Stopping device shell due to login";
StopDeviceShell();
}
InitializePresentation(user_shell_view_owner_);
const auto& settings_vector = UserShellSettings::GetSystemSettings();
if (active_user_shell_index_ >= settings_vector.size()) {
FXL_LOG(ERROR) << "Active user shell index is "
<< active_user_shell_index_ << ", but only "
<< settings_vector.size() << " user shells exist.";
return;
}
UpdatePresentation(settings_vector[active_user_shell_index_]);
}
// |UserProviderImpl::Delegate|
void DidLogout() override {
if (settings_.test) {
// TODO(MI4-1117): Integration tests currently expect device shell to
// always be running. So, if we're running under a test, DidLogin() will
// not shut down the device shell after login; thus this method doesn't
// need to re-start the device shell after a logout.
return;
}
FXL_DLOG(INFO) << "Re-starting device shell due to logout";
StartDeviceShell();
}
// |UserProviderImpl::Delegate|
fidl::InterfaceRequest<fuchsia::ui::viewsv1token::ViewOwner>
GetUserShellViewOwner(
fidl::InterfaceRequest<fuchsia::ui::viewsv1token::ViewOwner>) override {
return user_shell_view_owner_.is_bound()
? user_shell_view_owner_.Unbind().NewRequest()
: user_shell_view_owner_.NewRequest();
}
// |UserProviderImpl::Delegate|
fidl::InterfaceHandle<fuchsia::sys::ServiceProvider>
GetUserShellServiceProvider(
fidl::InterfaceHandle<fuchsia::sys::ServiceProvider>) override {
fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> handle;
service_namespace_.AddBinding(handle.NewRequest());
return handle;
}
// |KeyboardCaptureListenerHACK|
void OnEvent(fuchsia::ui::input::KeyboardEvent event) override {
switch (event.code_point) {
case ' ': {
SwapUserShell();
break;
}
case 's': {
SetNextShadowTechnique();
break;
}
case 'l':
ToggleClipping();
break;
default:
FXL_DLOG(INFO) << "Unknown keyboard event: codepoint="
<< event.code_point << ", modifiers=" << event.modifiers;
break;
}
}
void AddGlobalKeyboardShortcuts(
fuchsia::ui::policy::PresentationPtr& presentation) {
presentation->CaptureKeyboardEventHACK(
{
.code_point = ' ', // spacebar
.modifiers = fuchsia::ui::input::kModifierLeftControl,
},
keyboard_capture_listener_bindings_.AddBinding(this));
presentation->CaptureKeyboardEventHACK(
{
.code_point = 's',
.modifiers = fuchsia::ui::input::kModifierLeftControl,
},
keyboard_capture_listener_bindings_.AddBinding(this));
presentation->CaptureKeyboardEventHACK(
{
.code_point = 'l',
.modifiers = fuchsia::ui::input::kModifierRightAlt,
},
keyboard_capture_listener_bindings_.AddBinding(this));
}
void UpdatePresentation(const UserShellSettings& settings) {
if (settings.display_usage != fuchsia::ui::policy::DisplayUsage::kUnknown) {
FXL_DLOG(INFO) << "Setting display usage: "
<< fidl::ToUnderlying(settings.display_usage);
presentation_state_.presentation->SetDisplayUsage(settings.display_usage);
}
if (!std::isnan(settings.screen_width) &&
!std::isnan(settings.screen_height)) {
FXL_DLOG(INFO) << "Setting display size: " << settings.screen_width
<< " x " << settings.screen_height;
presentation_state_.presentation->SetDisplaySizeInMm(
settings.screen_width, settings.screen_height);
}
}
void SwapUserShell() {
if (UserShellSettings::GetSystemSettings().empty()) {
FXL_DLOG(INFO) << "No user shells has been defined";
return;
}
active_user_shell_index_ = (active_user_shell_index_ + 1) %
UserShellSettings::GetSystemSettings().size();
const auto& settings =
UserShellSettings::GetSystemSettings().at(active_user_shell_index_);
auto user_shell_config = fuchsia::modular::AppConfig::New();
user_shell_config->url = settings.name;
user_provider_impl_->SwapUserShell(std::move(*user_shell_config))->Then([] {
FXL_DLOG(INFO) << "Swapped user shell";
});
}
void SetNextShadowTechnique() {
using ShadowTechnique = fuchsia::ui::gfx::ShadowTechnique;
auto next_shadow_technique =
[](ShadowTechnique shadow_technique) -> ShadowTechnique {
switch (shadow_technique) {
case ShadowTechnique::UNSHADOWED:
return ShadowTechnique::SCREEN_SPACE;
case ShadowTechnique::SCREEN_SPACE:
return ShadowTechnique::SHADOW_MAP;
default:
FXL_LOG(ERROR) << "Unknown shadow technique: "
<< fidl::ToUnderlying(shadow_technique);
// Fallthrough
case ShadowTechnique::SHADOW_MAP:
case ShadowTechnique::MOMENT_SHADOW_MAP:
return ShadowTechnique::UNSHADOWED;
}
};
SetShadowTechnique(
next_shadow_technique(presentation_state_.shadow_technique));
}
void SetShadowTechnique(fuchsia::ui::gfx::ShadowTechnique shadow_technique) {
if (!presentation_state_.presentation)
return;
presentation_state_.shadow_technique = shadow_technique;
FXL_LOG(INFO) << "Setting shadow technique to "
<< fidl::ToUnderlying(presentation_state_.shadow_technique);
fuchsia::ui::gfx::RendererParam param;
param.set_shadow_technique(presentation_state_.shadow_technique);
auto renderer_params =
fidl::VectorPtr<fuchsia::ui::gfx::RendererParam>::New(0);
renderer_params.push_back(std::move(param));
presentation_state_.presentation->SetRendererParams(
std::move(renderer_params));
}
void ToggleClipping() {
if (!presentation_state_.presentation)
return;
FXL_DLOG(INFO) << "Toggling clipping";
presentation_state_.clipping_enabled =
!presentation_state_.clipping_enabled;
presentation_state_.presentation->EnableClipping(
presentation_state_.clipping_enabled);
}
const Settings& settings_; // Not owned nor copied.
AsyncHolder<UserProviderImpl> user_provider_impl_;
std::shared_ptr<component::StartupContext> const context_;
fuchsia::modular::DeviceRunnerMonitorPtr monitor_;
std::function<void()> on_shutdown_;
fidl::Binding<fuchsia::modular::DeviceShellContext>
device_shell_context_binding_;
fidl::Binding<fuchsia::modular::auth::AccountProviderContext>
account_provider_context_binding_;
std::unique_ptr<AppClient<fuchsia::modular::auth::AccountProvider>>
account_provider_;
std::unique_ptr<AppClient<fuchsia::modular::Lifecycle>>
token_manager_factory_app_;
fuchsia::auth::TokenManagerFactoryPtr token_manager_factory_;
bool device_shell_running_{};
std::unique_ptr<AppClient<fuchsia::modular::Lifecycle>> device_shell_app_;
fuchsia::modular::DeviceShellPtr device_shell_;
fidl::BindingSet<fuchsia::ui::policy::KeyboardCaptureListenerHACK>
keyboard_capture_listener_bindings_;
fuchsia::ui::viewsv1token::ViewOwnerPtr user_shell_view_owner_;
struct {
fuchsia::ui::policy::PresentationPtr presentation;
fidl::BindingSet<fuchsia::ui::policy::Presentation> bindings;
fuchsia::ui::gfx::ShadowTechnique shadow_technique =
fuchsia::ui::gfx::ShadowTechnique::UNSHADOWED;
bool clipping_enabled{};
} presentation_state_;
component::ServiceNamespace service_namespace_;
std::vector<UserShellSettings>::size_type active_user_shell_index_{};
FXL_DISALLOW_COPY_AND_ASSIGN(DeviceRunnerApp);
};
fit::deferred_action<fit::closure> SetupCobalt(
Settings& settings, async_dispatcher_t* dispatcher,
component::StartupContext* context) {
if (settings.disable_statistics) {
return fit::defer<fit::closure>([] {});
}
return InitializeCobalt(dispatcher, context);
};
} // namespace modular
int main(int argc, const char** argv) {
auto command_line = fxl::CommandLineFromArgcArgv(argc, argv);
if (command_line.HasOption("help")) {
std::cout << modular::Settings::GetUsage() << std::endl;
return 0;
}
modular::Settings settings(command_line);
async::Loop loop(&kAsyncLoopConfigAttachToThread);
trace::TraceProvider trace_provider(loop.dispatcher());
auto context = std::shared_ptr<component::StartupContext>(
component::StartupContext::CreateFromStartupInfo());
fit::deferred_action<fit::closure> cobalt_cleanup = modular::SetupCobalt(
settings, std::move(loop.dispatcher()), context.get());
modular::DeviceRunnerApp app(settings, context, [&loop, &cobalt_cleanup] {
cobalt_cleanup.call();
loop.Quit();
});
loop.Run();
return 0;
}