blob: 30136a7a931bd5daa17f408f5827b1f95beae590 [file] [log] [blame]
// Copyright 2013 The Flutter 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 "dart_test_component_controller_v2.h"
#include <fcntl.h>
#include <fml/logging.h>
#include <fuchsia/sys/test/cpp/fidl.h>
#include <lib/async-loop/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/async/default.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/namespace.h>
#include <lib/fidl/cpp/string.h>
#include <lib/fpromise/barrier.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/syslog/global.h>
#include <lib/zx/clock.h>
#include <lib/zx/thread.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <zircon/status.h>
#include <regex>
#include <utility>
#include "runtime/dart/utils/files.h"
#include "runtime/dart/utils/handle_exception.h"
#include "runtime/dart/utils/inlines.h"
#include "runtime/dart/utils/tempfs.h"
#include "third_party/dart/runtime/include/dart_tools_api.h"
#include "third_party/tonic/converter/dart_converter.h"
#include "third_party/tonic/dart_message_handler.h"
#include "third_party/tonic/dart_microtask_queue.h"
#include "third_party/tonic/dart_state.h"
#include "third_party/tonic/logging/dart_error.h"
#include "third_party/tonic/logging/dart_invoke.h"
#include "builtin_libraries.h"
#include "logging.h"
using tonic::ToDart;
namespace dart_runner {
namespace {
constexpr char kTmpPath[] = "/tmp";
constexpr zx::duration kIdleWaitDuration = zx::sec(2);
constexpr zx::duration kIdleNotifyDuration = zx::msec(500);
constexpr zx::duration kIdleSlack = zx::sec(1);
void AfterTask(async_loop_t*, void*) {
tonic::DartMicrotaskQueue* queue =
tonic::DartMicrotaskQueue::GetForCurrentThread();
// Verify that the queue exists, as this method could have been called back as
// part of the exit routine, after the destruction of the microtask queue.
if (queue) {
queue->RunMicrotasks();
}
}
constexpr async_loop_config_t kLoopConfig = {
.default_accessors =
{
.getter = async_get_default_dispatcher,
.setter = async_set_default_dispatcher,
},
.make_default_for_current_thread = true,
.epilogue = &AfterTask,
};
// Find the last path of the component.
// fuchsia-pkg://fuchsia.com/hello_dart#meta/hello_dart.cmx -> hello_dart.cmx
std::string GetLabelFromUrl(const std::string& url) {
for (size_t i = url.length() - 1; i > 0; i--) {
if (url[i] == '/') {
return url.substr(i + 1, url.length() - 1);
}
}
return url;
}
// Find the name of the component.
// fuchsia-pkg://fuchsia.com/hello_dart#meta/hello_dart.cm -> hello_dart
std::string GetComponentNameFromUrl(const std::string& url) {
const std::string label = GetLabelFromUrl(url);
for (size_t i = 0; i < label.length(); ++i) {
if (label[i] == '.') {
return label.substr(0, i);
}
}
return label;
}
} // namespace
DartTestComponentControllerV2::DartTestComponentControllerV2(
fuchsia::component::runner::ComponentStartInfo start_info,
std::shared_ptr<sys::ServiceDirectory> runner_incoming_services,
fidl::InterfaceRequest<fuchsia::component::runner::ComponentController>
controller,
DoneCallback done_callback)
: loop_(new async::Loop(&kLoopConfig)),
label_(GetLabelFromUrl(start_info.resolved_url())),
url_(std::move(start_info.resolved_url())),
runner_incoming_services_(runner_incoming_services),
start_info_(std::move(start_info)),
binding_(this),
done_callback_(std::move(done_callback)) {
// TODO(fxb/84537): This data path is configured based how we build Flutter
// applications in tree currently, but the way we build the Flutter
// application may change. We should avoid assuming the data path and let the
// CML file specify this data path instead.
const std::string component_name = GetComponentNameFromUrl(url_);
FX_LOGF(INFO, LOG_TAG, "Component Name from URL: %s.",
component_name.c_str());
data_path_ = "pkg/data/" + component_name;
if (controller.is_valid()) {
binding_.Bind(std::move(controller));
binding_.set_error_handler([this](zx_status_t status) { Kill(); });
} else {
FX_LOG(ERROR, LOG_TAG,
"Fuchsia component controller endpoint is not valid.");
}
}
DartTestComponentControllerV2::~DartTestComponentControllerV2() {
if (namespace_) {
fdio_ns_destroy(namespace_);
namespace_ = nullptr;
}
close(stdout_fd_);
close(stderr_fd_);
}
void DartTestComponentControllerV2::SetUp() {
// Name the thread after the url of the component being launched.
zx::thread::self()->set_property(ZX_PROP_NAME, label_.c_str(), label_.size());
Dart_SetThreadName(label_.c_str());
if (!CreateAndBindNamespace()) {
return;
}
if (SetUpFromAppSnapshot()) {
FX_LOGF(INFO, LOG_TAG, "%s is running from an app snapshot", url_.c_str());
} else if (SetUpFromKernel()) {
FX_LOGF(INFO, LOG_TAG, "%s is running from kernel", url_.c_str());
} else {
FX_LOGF(ERROR, LOG_TAG, "Failed to set up component controller for %s.",
url_.c_str());
return;
// return false;
}
// return true;
// suite_ = std::make_unique<Suite>(url_, loop_);
suite_context_ = sys::ComponentContext::Create();
suite_context_->outgoing()->AddPublicService(
suite_bindings_.GetHandler(this, loop_->dispatcher()));
suite_context_->outgoing()->Serve(
start_info_.mutable_outgoing_dir()->TakeChannel(), loop_->dispatcher());
}
bool DartTestComponentControllerV2::CreateAndBindNamespace() {
if (!start_info_.has_ns()) {
FX_LOG(ERROR, LOG_TAG, "Component start info does not have a namespace.");
return false;
}
const zx_status_t ns_create_status = fdio_ns_create(&namespace_);
if (ns_create_status != ZX_OK) {
FX_LOGF(ERROR, LOG_TAG, "Failed to create namespace: %s",
zx_status_get_string(ns_create_status));
}
dart_utils::RunnerTemp::SetupComponent(namespace_);
// Bind each directory in start_info's namespace to the controller's namespace
// instance.
for (auto& ns_entry : *start_info_.mutable_ns()) {
// TODO(akbiggs): Under what circumstances does a namespace entry not
// have a path or directory? Should we log an error for these?
if (!ns_entry.has_path() || !ns_entry.has_directory()) {
continue;
}
if (ns_entry.path() == kTmpPath) {
// /tmp is covered by the local memfs.
continue;
}
// We move ownership of the directory & path since RAII is used to keep
// the handle open.
fidl::InterfaceHandle<::fuchsia::io::Directory> dir =
std::move(*ns_entry.mutable_directory());
const std::string path = std::move(*ns_entry.mutable_path());
const zx_status_t ns_bind_status =
fdio_ns_bind(namespace_, path.c_str(), dir.TakeChannel().release());
if (ns_bind_status != ZX_OK) {
FX_LOGF(ERROR, LOG_TAG, "Failed to bind %s to namespace: %s",
path.c_str(), zx_status_get_string(ns_bind_status));
return false;
}
}
return true;
}
bool DartTestComponentControllerV2::SetUpFromKernel() {
dart_utils::MappedResource manifest;
if (!dart_utils::MappedResource::LoadFromNamespace(
namespace_, data_path_ + "/app.dilplist", manifest)) {
return false;
}
if (!dart_utils::MappedResource::LoadFromNamespace(
nullptr, "/pkg/data/isolate_core_snapshot_data.bin",
isolate_snapshot_data_)) {
return false;
}
if (!dart_utils::MappedResource::LoadFromNamespace(
nullptr, "/pkg/data/isolate_core_snapshot_instructions.bin",
isolate_snapshot_instructions_, true /* executable */)) {
return false;
}
if (!CreateIsolate(isolate_snapshot_data_.address(),
isolate_snapshot_instructions_.address())) {
return false;
}
Dart_EnterScope();
std::string str(reinterpret_cast<const char*>(manifest.address()),
manifest.size());
Dart_Handle library = Dart_Null();
for (size_t start = 0; start < manifest.size();) {
size_t end = str.find("\n", start);
if (end == std::string::npos) {
FX_LOG(ERROR, LOG_TAG, "Malformed manifest");
Dart_ExitScope();
return false;
}
std::string path = data_path_ + "/" + str.substr(start, end - start);
start = end + 1;
dart_utils::MappedResource kernel;
if (!dart_utils::MappedResource::LoadFromNamespace(namespace_, path,
kernel)) {
FX_LOGF(ERROR, LOG_TAG, "Cannot load kernel from namespace: %s",
path.c_str());
Dart_ExitScope();
return false;
}
library = Dart_LoadLibraryFromKernel(kernel.address(), kernel.size());
if (Dart_IsError(library)) {
FX_LOGF(ERROR, LOG_TAG, "Cannot load library from kernel: %s",
Dart_GetError(library));
Dart_ExitScope();
return false;
}
kernel_peices_.emplace_back(std::move(kernel));
}
Dart_SetRootLibrary(library);
Dart_Handle result = Dart_FinalizeLoading(false);
if (Dart_IsError(result)) {
FX_LOGF(ERROR, LOG_TAG, "Failed to FinalizeLoading: %s",
Dart_GetError(result));
Dart_ExitScope();
return false;
}
return true;
}
bool DartTestComponentControllerV2::SetUpFromAppSnapshot() {
#if !defined(AOT_RUNTIME)
return false;
#else
// Load the ELF snapshot as available, and fall back to a blobs snapshot
// otherwise.
const uint8_t *isolate_data, *isolate_instructions;
if (elf_snapshot_.Load(namespace_, data_path_ + "/app_aot_snapshot.so")) {
isolate_data = elf_snapshot_.IsolateData();
isolate_instructions = elf_snapshot_.IsolateInstrs();
if (isolate_data == nullptr || isolate_instructions == nullptr) {
return false;
}
} else {
if (!dart_utils::MappedResource::LoadFromNamespace(
namespace_, data_path_ + "/isolate_snapshot_data.bin",
isolate_snapshot_data_)) {
return false;
}
if (!dart_utils::MappedResource::LoadFromNamespace(
namespace_, data_path_ + "/isolate_snapshot_instructions.bin",
isolate_snapshot_instructions_, true /* executable */)) {
return false;
}
}
return CreateIsolate(isolate_data, isolate_instructions);
#endif // defined(AOT_RUNTIME)
}
bool DartTestComponentControllerV2::CreateIsolate(
const uint8_t* isolate_snapshot_data,
const uint8_t* isolate_snapshot_instructions) {
// Create the isolate from the snapshot.
char* error = nullptr;
// TODO(dart_runner): Pass if we start using tonic's loader.
intptr_t namespace_fd = -1;
// Freed in IsolateShutdownCallback.
auto state = new std::shared_ptr<tonic::DartState>(new tonic::DartState(
namespace_fd, [this](Dart_Handle result) { MessageEpilogue(result); }));
isolate_ = Dart_CreateIsolateGroup(
url_.c_str(), label_.c_str(), isolate_snapshot_data,
isolate_snapshot_instructions, nullptr /* flags */, state, state, &error);
if (!isolate_) {
FX_LOGF(ERROR, LOG_TAG, "Dart_CreateIsolateGroup failed: %s", error);
return false;
}
state->get()->SetIsolate(isolate_);
tonic::DartMessageHandler::TaskDispatcher dispatcher =
[loop = loop_.get()](auto callback) {
async::PostTask(loop->dispatcher(), std::move(callback));
};
state->get()->message_handler().Initialize(dispatcher);
state->get()->SetReturnCodeCallback(
[this](uint32_t return_code) { return_code_ = return_code; });
return true;
}
// void DartTestComponentControllerV2::Run() {
// async::PostTask(loop_->dispatcher(), [loop = loop_.get(), app = this] {
// if (!app->RunDartMain()) {
// loop->Quit();
// }
// });
// loop_->Run();
// if (binding_.is_bound()) {
// // From the documentation for ComponentController, ZX_OK should be sent
// when
// // the ComponentController receives a termination request. However, if
// the
// // component exited with a non-zero return code, we indicate this by
// sending
// // an INTERNAL epitaph instead.
// //
// // TODO(fxb/86666): Communicate return code from the ComponentController
// // once v2 has support.
// if (return_code_ == 0) {
// binding_.Close(ZX_OK);
// } else {
// FML_LOG(ERROR) << "Component exited with non-zero return code: "
// << return_code_;
// binding_.Close(zx_status_t(fuchsia::component::Error::INTERNAL));
// }
// }
// }
// |fuchsia::test::Suite|
DartTestComponentControllerV2::CaseIterator::CaseIterator(
fidl::InterfaceRequest<fuchsia::sys::test::CaseIterator> request,
async_dispatcher_t* dispatcher,
fit::function<void(CaseIterator*)> done_callback)
: binding_(this, std::move(request), dispatcher),
done_callback_(std::move(done_callback)) {}
static const char kTestCaseName[] = "dart_test";
void DartTestComponentControllerV2::CaseIterator::GetNext(
GetNextCallback callback) {
FML_LOG(INFO) << "DartTestComponentControllerV2 GetNext() called";
if (get_next_call_count == 0) {
fuchsia::sys::test::Case test_case;
test_case.set_name(std::string(kTestCaseName));
test_case.set_enabled(true);
std::vector<fuchsia::sys::test::Case> cases;
cases.push_back(std::move(test_case));
callback(std::move(cases));
get_next_call_count++;
} else {
std::vector<fuchsia::sys::test::Case> cases;
callback(std::move(cases));
// this would be removed
done_callback_(this);
}
}
void DartTestComponentControllerV2::GetTests(
fidl::InterfaceRequest<fuchsia::sys::test::CaseIterator> iterator) {
FML_LOG(INFO) << "DART RUNNER HIT SUITE GetTests()";
auto case_iterator =
std::make_unique<CaseIterator>(std::move(iterator), loop_->dispatcher(),
[this](CaseIterator* case_iterator) {
RemoveCaseInterator(case_iterator);
});
case_iterators_.emplace(case_iterator.get(), std::move(case_iterator));
}
std::unique_ptr<DartTestComponentControllerV2::CaseIterator>
DartTestComponentControllerV2::RemoveCaseInterator(
CaseIterator* case_iterator) {
auto it = case_iterators_.find(case_iterator);
std::unique_ptr<DartTestComponentControllerV2::CaseIterator>
case_iterator_ptr;
if (it != case_iterators_.end()) {
case_iterator_ptr = std::move(it->second);
case_iterators_.erase(it);
}
return case_iterator_ptr;
}
// |fuchsia::test::Suite|
void DartTestComponentControllerV2::Run(
std::vector<fuchsia::sys::test::Invocation> tests,
fuchsia::sys::test::RunOptions options,
fidl::InterfaceHandle<fuchsia::sys::test::RunListener> listener) {
FML_LOG(INFO) << "DART RUNNER HIT SUITE Run()";
std::vector<std::string> args;
if (options.has_arguments()) {
args = std::move(*options.mutable_arguments());
}
if (options.has_parallel()) {
FML_LOG(WARNING) << "Ignoring 'parallel'. Pass test specific flags, eg: "
"for rust test pass in "
"--test-threads="
<< options.parallel();
}
auto listener_proxy = listener.Bind();
fpromise::barrier barrier;
for (auto it = tests.begin(); it != tests.end(); it++) {
auto invocation = std::move(*it);
std::string test_case_name;
if (invocation.has_name()) {
test_case_name = invocation.name();
}
FML_LOG(INFO) << "DART RUNNER HIT SUITE RUN - TEST CASE: "
<< test_case_name;
zx::socket out, err, out_client, err_client;
auto status = zx::socket::create(0, &out, &out_client);
if (status != ZX_OK) {
FML_LOG(FATAL) << "cannot create socket: "
<< zx_status_get_string(status);
}
status = zx::socket::create(0, &err, &err_client);
if (status != ZX_OK) {
FML_LOG(FATAL) << "cannot create socket: "
<< zx_status_get_string(status);
}
fidl::InterfacePtr<fuchsia::sys::test::CaseListener> case_listener;
fuchsia::sys::test::StdHandles std_handles;
std_handles.set_err(std::move(err_client));
std_handles.set_out(std::move(out_client));
listener_proxy->OnTestCaseStarted(std::move(invocation),
std::move(std_handles),
case_listener.NewRequest());
async::PostTask(loop_->dispatcher(), [loop = loop_.get(), app = this] {
if (!app->RunDartMain()) {
loop->Quit();
}
});
loop_->Run();
auto ret_status = return_code_ == 0 ? fuchsia::sys::test::Status::PASSED
: fuchsia::sys::test::Status::FAILED;
fuchsia::sys::test::Result result;
result.set_status(ret_status);
FML_LOG(INFO) << "Sending finished event for dart tests";
case_listener->Finished(std::move(result));
}
if (binding_.is_bound()) {
// From the documentation for ComponentController, ZX_OK should be sent when
// the ComponentController receives a termination request. However, if the
// component exited with a non-zero return code, we indicate this by sending
// an INTERNAL epitaph instead.
//
// TODO(fxb/86666): Communicate return code from the ComponentController
// once v2 has support.
if (return_code_ == 0) {
binding_.Close(ZX_OK);
} else {
FML_LOG(ERROR) << "Component exited with non-zero return code: "
<< return_code_;
binding_.Close(zx_status_t(fuchsia::component::Error::INTERNAL));
}
}
}
bool DartTestComponentControllerV2::RunDartMain() {
FML_CHECK(namespace_ != nullptr);
Dart_EnterScope();
tonic::DartMicrotaskQueue::StartForCurrentThread();
// TODO(fxb/88384): Create a file descriptor for each component that is
// launched and listen for anything that is written to the component. When
// something is written to the component, forward that message along to the
// Fuchsia logger and decorate it with the tag that it came from the
// component.
stdout_fd_ = fileno(stdout);
stderr_fd_ = fileno(stderr);
fidl::InterfaceRequest<fuchsia::io::Directory> outgoing_dir =
std::move(*start_info_.mutable_outgoing_dir());
FML_LOG(INFO) << "DART TEST COMPONENT CONTROLLER: RUNMAIN()";
FML_LOG(INFO) << url_;
InitBuiltinLibrariesForIsolate(
url_, namespace_, stdout_fd_, stderr_fd_, nullptr /* environment */,
outgoing_dir.TakeChannel(), false /* service_isolate */);
Dart_ExitScope();
Dart_ExitIsolate();
char* error = Dart_IsolateMakeRunnable(isolate_);
if (error != nullptr) {
Dart_EnterIsolate(isolate_);
Dart_ShutdownIsolate();
FX_LOGF(ERROR, LOG_TAG, "Unable to make isolate runnable: %s", error);
free(error);
return false;
}
Dart_EnterIsolate(isolate_);
Dart_EnterScope();
// TODO(fxb/88383): Support argument passing.
Dart_Handle corelib = Dart_LookupLibrary(ToDart("dart:core"));
Dart_Handle string_type =
Dart_GetNonNullableType(corelib, ToDart("String"), 0, NULL);
Dart_Handle dart_arguments =
Dart_NewListOfTypeFilled(string_type, Dart_EmptyString(), 0);
if (Dart_IsError(dart_arguments)) {
FX_LOGF(ERROR, LOG_TAG, "Failed to allocate Dart arguments list: %s",
Dart_GetError(dart_arguments));
Dart_ExitScope();
return false;
}
Dart_Handle user_main = Dart_GetField(Dart_RootLibrary(), ToDart("main"));
if (Dart_IsError(user_main)) {
FX_LOGF(ERROR, LOG_TAG,
"Failed to locate user_main in the root library: %s",
Dart_GetError(user_main));
Dart_ExitScope();
return false;
}
Dart_Handle fuchsia_lib = Dart_LookupLibrary(tonic::ToDart("dart:fuchsia"));
if (Dart_IsError(fuchsia_lib)) {
FX_LOGF(ERROR, LOG_TAG, "Failed to locate dart:fuchsia: %s",
Dart_GetError(fuchsia_lib));
Dart_ExitScope();
return false;
}
Dart_Handle main_result = tonic::DartInvokeField(
fuchsia_lib, "_runUserMainForDartRunner", {user_main, dart_arguments});
if (Dart_IsError(main_result)) {
auto dart_state = tonic::DartState::Current();
if (!dart_state->has_set_return_code()) {
// The program hasn't set a return code meaning this exit is unexpected.
FX_LOG(ERROR, LOG_TAG, Dart_GetError(main_result));
return_code_ = tonic::GetErrorExitCode(main_result);
dart_utils::HandleIfException(runner_incoming_services_, url_,
main_result);
}
Dart_ExitScope();
return false;
}
Dart_ExitScope();
return true;
}
void DartTestComponentControllerV2::Kill() {
FML_LOG(INFO) << "DartTestComponentControllerV2 KILL CALLED";
if (Dart_CurrentIsolate()) {
tonic::DartMicrotaskQueue* queue =
tonic::DartMicrotaskQueue::GetForCurrentThread();
if (queue) {
queue->Destroy();
}
loop_->Quit();
// TODO(rosswang): The docs warn of threading issues if doing this again,
// but without this, attempting to shut down the isolate finalizes app
// contexts that can't tell a shutdown is in progress and so fatal.
Dart_SetMessageNotifyCallback(nullptr);
Dart_ShutdownIsolate();
}
done_callback_(this);
}
void DartTestComponentControllerV2::MessageEpilogue(Dart_Handle result) {
auto dart_state = tonic::DartState::Current();
// If the Dart program has set a return code, then it is intending to shut
// down by way of a fatal error, and so there is no need to override
// return_code_.
if (dart_state->has_set_return_code()) {
Dart_ShutdownIsolate();
return;
}
dart_utils::HandleIfException(runner_incoming_services_, url_, result);
// Otherwise, see if there was any other error.
return_code_ = tonic::GetErrorExitCode(result);
if (return_code_ != 0) {
Dart_ShutdownIsolate();
return;
}
idle_start_ = zx::clock::get_monotonic();
zx_status_t status =
idle_timer_.set(idle_start_ + kIdleWaitDuration, kIdleSlack);
if (status != ZX_OK) {
FX_LOGF(INFO, LOG_TAG, "Idle timer set failed: %s",
zx_status_get_string(status));
}
}
void DartTestComponentControllerV2::Stop() {
Kill();
}
void DartTestComponentControllerV2::OnIdleTimer(
async_dispatcher_t* dispatcher,
async::WaitBase* wait,
zx_status_t status,
const zx_packet_signal* signal) {
if ((status != ZX_OK) || !(signal->observed & ZX_TIMER_SIGNALED) ||
!Dart_CurrentIsolate()) {
// Timer closed or isolate shutdown.
return;
}
zx::time deadline = idle_start_ + kIdleWaitDuration;
zx::time now = zx::clock::get_monotonic();
if (now >= deadline) {
// No Dart message has been processed for kIdleWaitDuration: assume we'll
// stay idle for kIdleNotifyDuration.
Dart_NotifyIdle((now + kIdleNotifyDuration).get());
idle_start_ = zx::time(0);
idle_timer_.cancel(); // De-assert signal.
} else {
// Early wakeup or message pushed idle time forward: reschedule.
zx_status_t status = idle_timer_.set(deadline, kIdleSlack);
if (status != ZX_OK) {
FX_LOGF(INFO, LOG_TAG, "Idle timer set failed: %s",
zx_status_get_string(status));
}
}
wait->Begin(dispatcher); // ignore errors
}
} // namespace dart_runner