| // Copyright 2018 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 <fuchsia/sys/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/default.h> |
| #include <lib/async/dispatcher.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <lib/fidl/cpp/binding_set.h> |
| #include <lib/sys/cpp/file_descriptor.h> |
| #include <lib/sys/cpp/service_directory.h> |
| #include <lib/sys/cpp/testing/enclosing_environment.h> |
| #include <lib/sys/cpp/testing/test_with_environment.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <unistd.h> |
| #include <zircon/rights.h> |
| |
| #include <fstream> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include <fidl/examples/echo/cpp/fidl.h> |
| #include <fs/pseudo_dir.h> |
| #include <fs/synchronous_vfs.h> |
| #include <test/appmgr/integration/cpp/fidl.h> |
| |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "src/lib/files/glob.h" |
| #include "src/lib/files/scoped_temp_dir.h" |
| #include "src/lib/fxl/logging.h" |
| #include "src/sys/appmgr/component_controller_impl.h" |
| #include "src/sys/appmgr/integration_tests/util/data_file_reader_writer_util.h" |
| #include "src/sys/appmgr/realm.h" |
| #include "src/sys/appmgr/util.h" |
| |
| namespace component { |
| namespace { |
| |
| using fuchsia::sys::TerminationReason; |
| |
| using ::testing::AnyOf; |
| using ::testing::Eq; |
| |
| using sys::testing::EnclosingEnvironment; |
| using sys::testing::TestWithEnvironment; |
| using test::appmgr::integration::DataFileReaderWriterPtr; |
| |
| class RealmTest : virtual public TestWithEnvironment { |
| protected: |
| void SetUp() override { |
| TestWithEnvironment::SetUp(); |
| OpenNewOutFile(); |
| } |
| |
| void OpenNewOutFile() { |
| ASSERT_TRUE(tmp_dir_.NewTempFile(&out_file_)); |
| outf_ = fileno(std::fopen(out_file_.c_str(), "w")); |
| } |
| |
| std::string ReadOutFile() { |
| std::string out; |
| if (!files::ReadFileToString(out_file_, &out)) { |
| FXL_LOG(ERROR) << "Could not read output file " << out_file_; |
| return ""; |
| } |
| return out; |
| } |
| |
| fuchsia::sys::LaunchInfo CreateLaunchInfo(const std::string& url, |
| zx::channel directory_request = zx::channel(), |
| const std::vector<std::string>& args = {}) { |
| fuchsia::sys::LaunchInfo launch_info; |
| launch_info.url = url; |
| launch_info.arguments = args; |
| if (directory_request.is_valid()) { |
| launch_info.directory_request = std::move(directory_request); |
| } |
| launch_info.out = sys::CloneFileDescriptor(outf_); |
| launch_info.err = sys::CloneFileDescriptor(STDERR_FILENO); |
| return launch_info; |
| } |
| |
| fuchsia::sys::ComponentControllerPtr RunComponent(EnclosingEnvironment* enclosing_environment, |
| const std::string& url, |
| zx::channel directory_request = zx::channel(), |
| const std::vector<std::string>& args = {}) { |
| return enclosing_environment->CreateComponent( |
| CreateLaunchInfo(url, std::move(directory_request), std::move(args))); |
| } |
| |
| private: |
| files::ScopedTempDir tmp_dir_; |
| std::string out_file_; |
| int outf_; |
| }; |
| |
| constexpr char kRealm[] = "realmintegrationtest"; |
| |
| TEST_F(RealmTest, Resolve) { |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, CreateServices()); |
| |
| auto resolver = enclosing_environment->ConnectToService<fuchsia::process::Resolver>(); |
| |
| bool wait = false; |
| resolver->Resolve( |
| "fuchsia-pkg://fuchsia.com/appmgr_integration_tests#test/" |
| "appmgr_realm_integration_tests", |
| [&wait](zx_status_t status, zx::vmo binary, |
| fidl::InterfaceHandle<fuchsia::ldsvc::Loader> loader) { |
| wait = true; |
| |
| ASSERT_EQ(ZX_OK, status); |
| |
| std::string expect; |
| files::ReadFileToString("/pkg/test/appmgr_realm_integration_tests", &expect); |
| ASSERT_FALSE(expect.empty()); |
| |
| std::vector<char> buf(expect.length()); |
| ASSERT_EQ(ZX_OK, binary.read(buf.data(), 0, buf.size())); |
| std::string actual(buf.begin(), buf.end()); |
| |
| ASSERT_EQ(expect, actual); |
| }); |
| RunLoopUntil([&wait] { return wait; }); |
| } |
| |
| TEST_F(RealmTest, LaunchNonExistentComponent) { |
| auto env_services = CreateServices(); |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, std::move(env_services)); |
| WaitForEnclosingEnvToStart(enclosing_environment.get()); |
| |
| // try to launch file url. |
| auto controller1 = RunComponent(enclosing_environment.get(), "does_not_exist"); |
| bool wait = false; |
| controller1.events().OnTerminated = [&wait](int64_t return_code, |
| fuchsia::sys::TerminationReason reason) { |
| wait = true; |
| EXPECT_EQ(reason, fuchsia::sys::TerminationReason::PACKAGE_NOT_FOUND); |
| }; |
| RunLoopUntil([&wait] { return wait; }); |
| |
| // try to launch pkg url. |
| auto controller2 = RunComponent(enclosing_environment.get(), |
| "fuchsia-pkg://fuchsia.com/does_not_exist#meta/some.cmx"); |
| wait = false; |
| controller2.events().OnTerminated = [&wait](int64_t return_code, |
| fuchsia::sys::TerminationReason reason) { |
| wait = true; |
| EXPECT_EQ(reason, fuchsia::sys::TerminationReason::PACKAGE_NOT_FOUND); |
| }; |
| RunLoopUntil([&wait] { return wait; }); |
| } |
| |
| // This test exercises the fact that two components should be in separate jobs, |
| // and thus when one component controller kills its job due to a .Kill() call |
| // the other component should run uninterrupted. |
| TEST_F(RealmTest, CreateTwoKillOne) { |
| // launch component as a service. |
| auto env_services = CreateServices(); |
| ASSERT_EQ(ZX_OK, env_services->AddServiceWithLaunchInfo( |
| CreateLaunchInfo("fuchsia-pkg://fuchsia.com/" |
| "echo_server_cpp#meta/echo_server_cpp.cmx"), |
| fidl::examples::echo::Echo::Name_)); |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, std::move(env_services)); |
| WaitForEnclosingEnvToStart(enclosing_environment.get()); |
| // launch component normally |
| auto controller1 = |
| RunComponent(enclosing_environment.get(), |
| "fuchsia-pkg://fuchsia.com/echo_server_cpp#meta/echo_server_cpp.cmx"); |
| |
| // make sure echo service is running. |
| fidl::examples::echo::EchoPtr echo; |
| enclosing_environment->ConnectToService(echo.NewRequest()); |
| const std::string message = "CreateTwoKillOne"; |
| fidl::StringPtr ret_msg; |
| echo->EchoString(message, [&](::fidl::StringPtr retval) { ret_msg = retval; }); |
| RunLoopUntil([&] { return ret_msg.has_value(); }); |
| |
| // Kill one of the two components, make sure it's exited via Wait |
| bool wait = false; |
| controller1.events().OnTerminated = |
| [&wait](int64_t return_code, fuchsia::sys::TerminationReason reason) { wait = true; }; |
| controller1->Kill(); |
| RunLoopUntil([&wait] { return wait; }); |
| |
| // Make sure the second component is still running. |
| ret_msg.reset(); |
| echo->EchoString(message, [&](::fidl::StringPtr retval) { ret_msg = retval; }); |
| RunLoopUntil([&] { return ret_msg.has_value(); }); |
| } |
| |
| TEST_F(RealmTest, KillRealmKillsComponent) { |
| auto env_services = CreateServices(); |
| ASSERT_EQ(ZX_OK, env_services->AddServiceWithLaunchInfo( |
| CreateLaunchInfo("fuchsia-pkg://fuchsia.com/" |
| "echo_server_cpp#meta/echo_server_cpp.cmx"), |
| fidl::examples::echo::Echo::Name_)); |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, std::move(env_services)); |
| WaitForEnclosingEnvToStart(enclosing_environment.get()); |
| |
| // make sure echo service is running. |
| fidl::examples::echo::EchoPtr echo; |
| enclosing_environment->ConnectToService(echo.NewRequest()); |
| const std::string message = "CreateTwoKillOne"; |
| fidl::StringPtr ret_msg; |
| echo->EchoString(message, [&](::fidl::StringPtr retval) { ret_msg = retval; }); |
| RunLoopUntil([&] { return ret_msg.has_value(); }); |
| |
| bool killed = false; |
| echo.set_error_handler([&](zx_status_t status) { killed = true; }); |
| enclosing_environment->Kill(); |
| RunLoopUntil([&] { return enclosing_environment->is_running(); }); |
| // send a msg, without that error handler won't be called. |
| echo->EchoString(message, [&](::fidl::StringPtr retval) { ret_msg = retval; }); |
| RunLoopUntil([&] { return killed; }); |
| } |
| |
| TEST_F(RealmTest, EnvironmentControllerRequired) { |
| fuchsia::sys::EnvironmentPtr env; |
| real_env()->CreateNestedEnvironment(env.NewRequest(), /* controller = */ nullptr, kRealm, |
| /* additional_services = */ nullptr, |
| fuchsia::sys::EnvironmentOptions{}); |
| |
| zx_status_t env_status = ZX_OK; |
| env.set_error_handler([&](zx_status_t status) { env_status = status; }); |
| |
| RunLoopUntil([&] { return env_status != ZX_OK; }); |
| } |
| |
| TEST_F(RealmTest, EnvironmentLabelMustBeUnique) { |
| // Create first environment with label kRealm using EnclosingEnvironment since |
| // that's easy. |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, CreateServices()); |
| |
| // Can't use EnclosingEnvironment here since there's no way to discern between |
| // 'not yet created' and 'failed to create'. This also lets us check the |
| // specific status returned. |
| fuchsia::sys::EnvironmentPtr env; |
| fuchsia::sys::EnvironmentControllerPtr env_controller; |
| |
| zx_status_t env_status, env_controller_status; |
| env.set_error_handler([&](zx_status_t status) { env_status = status; }); |
| env_controller.set_error_handler([&](zx_status_t status) { env_controller_status = status; }); |
| |
| // Same environment label as EnclosingEnvironment created above. |
| real_env()->CreateNestedEnvironment(env.NewRequest(), env_controller.NewRequest(), kRealm, |
| nullptr, fuchsia::sys::EnvironmentOptions{}); |
| |
| RunLoopUntil([&] { return env_status == ZX_ERR_BAD_STATE; }); |
| RunLoopUntil([&] { return env_controller_status == ZX_ERR_BAD_STATE; }); |
| } |
| |
| TEST_F(RealmTest, RealmJobProvider) { |
| fuchsia::sys::JobProviderSyncPtr ptr; |
| fdio_service_connect("/hub/job", ptr.NewRequest().TakeChannel().release()); |
| |
| zx::job job; |
| ASSERT_EQ(ZX_OK, ptr->GetJob(&job)); |
| |
| // check that we can get properties. |
| char name[ZX_MAX_NAME_LEN]; |
| ASSERT_EQ(ZX_OK, job.get_property(ZX_PROP_NAME, name, sizeof(name))); |
| |
| EXPECT_THAT(name, ::testing::StartsWith("test_env")); |
| zx_koid_t koid; |
| size_t actual; |
| size_t avail_count; |
| ASSERT_EQ(ZX_OK, |
| job.get_info(ZX_INFO_JOB_CHILDREN, &koid, sizeof(zx_koid_t), &actual, &avail_count)); |
| ASSERT_EQ(1u, actual); |
| |
| // check that we can get children |
| zx::job child_job; |
| ASSERT_EQ(ZX_OK, job.get_child(koid, ZX_RIGHT_SAME_RIGHTS, &child_job)); |
| } |
| |
| TEST_F(RealmTest, RealmDiesWhenItsJobDies) { |
| auto env_services = CreateServices(); |
| |
| ASSERT_EQ(ZX_OK, env_services->AddServiceWithLaunchInfo( |
| CreateLaunchInfo("fuchsia-pkg://fuchsia.com/" |
| "echo_server_cpp#meta/echo_server_cpp.cmx"), |
| fidl::examples::echo::Echo::Name_)); |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, std::move(env_services)); |
| WaitForEnclosingEnvToStart(enclosing_environment.get()); |
| |
| // make sure echo service is running. |
| fidl::examples::echo::EchoPtr echo; |
| enclosing_environment->ConnectToService(echo.NewRequest()); |
| const std::string message = "some_msg"; |
| fidl::StringPtr ret_msg; |
| echo->EchoString(message, [&](::fidl::StringPtr retval) { ret_msg = retval; }); |
| RunLoopUntil([&] { return ret_msg.has_value(); }); |
| |
| fuchsia::sys::JobProviderSyncPtr ptr; |
| files::Glob glob(std::string("/hub/r/") + kRealm + "/*/job"); |
| ASSERT_EQ(1u, glob.size()); |
| const std::string path = *glob.begin(); |
| fdio_service_connect(path.c_str(), ptr.NewRequest().TakeChannel().release()); |
| |
| zx::job job; |
| EXPECT_EQ(ZX_OK, ptr->GetJob(&job)); |
| ASSERT_EQ(ZX_OK, job.kill()); |
| |
| RunLoopUntil([&] { return !enclosing_environment->is_running(); }); |
| } |
| |
| TEST_F(RealmTest, EmptyRealmDiesWhenItsJobDies) { |
| auto env_services = CreateServices(); |
| |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, std::move(env_services)); |
| WaitForEnclosingEnvToStart(enclosing_environment.get()); |
| |
| fuchsia::sys::JobProviderSyncPtr ptr; |
| files::Glob glob(std::string("/hub/r/") + kRealm + "/*/job"); |
| ASSERT_EQ(1u, glob.size()); |
| const std::string path = *glob.begin(); |
| fdio_service_connect(path.c_str(), ptr.NewRequest().TakeChannel().release()); |
| |
| zx::job job; |
| EXPECT_EQ(ZX_OK, ptr->GetJob(&job)); |
| ASSERT_EQ(ZX_OK, job.kill()); |
| |
| RunLoopUntil([&] { return !enclosing_environment->is_running(); }); |
| } |
| |
| TEST_F(RealmTest, KillWorks) { |
| auto env_services = CreateServices(); |
| |
| auto enclosing_environment = CreateNewEnclosingEnvironment(kRealm, std::move(env_services)); |
| WaitForEnclosingEnvToStart(enclosing_environment.get()); |
| |
| std::string hub_path = std::string("/hub/r/") + kRealm; |
| // make sure realm was created |
| files::Glob glob1(hub_path); |
| ASSERT_EQ(1u, glob1.size()); |
| |
| bool killed = false; |
| enclosing_environment->Kill([&] { killed = true; }); |
| RunLoopUntil([&] { return killed; }); |
| |
| // make sure realm was really killed |
| files::Glob glob2(hub_path); |
| ASSERT_EQ(0u, glob2.size()); |
| } |
| |
| class EnvironmentOptionsTest : public RealmTest, |
| public component::testing::DataFileReaderWriterUtil {}; |
| |
| TEST_F(EnvironmentOptionsTest, DeleteStorageOnDeath) { |
| constexpr char kTestFileName[] = "some-test-file"; |
| constexpr char kTestFileContent[] = "the-best-file-content"; |
| |
| // Create an environment with 'delete_storage_on_death' option enabled. |
| zx::channel request; |
| auto services = sys::ServiceDirectory::CreateWithRequest(&request); |
| DataFileReaderWriterPtr util; |
| auto enclosing_environment = CreateNewEnclosingEnvironment( |
| kRealm, CreateServices(), fuchsia::sys::EnvironmentOptions{.delete_storage_on_death = true}); |
| auto ctrl = RunComponent(enclosing_environment.get(), |
| "fuchsia-pkg://fuchsia.com/persistent_storage_test_util#meta/util.cmx", |
| std::move(request)); |
| services->Connect(util.NewRequest()); |
| |
| // Write some arbitrary file content into the test util's "/data" dir, and |
| // verify that we can read it back. |
| ASSERT_EQ(WriteFileSync(util, kTestFileName, kTestFileContent), ZX_OK); |
| ASSERT_EQ(ReadFileSync(util, kTestFileName).value_or(""), kTestFileContent); |
| |
| // Kill the environment, which should automatically delete any persistent |
| // storage it owns. |
| bool killed = false; |
| enclosing_environment->Kill([&] { killed = true; }); |
| RunLoopUntil([&] { return killed; }); |
| |
| // Recreate the environment and component using the same environment label. |
| services = sys::ServiceDirectory::CreateWithRequest(&request); |
| enclosing_environment = CreateNewEnclosingEnvironment(kRealm, CreateServices()); |
| ctrl = RunComponent(enclosing_environment.get(), |
| "fuchsia-pkg://fuchsia.com/persistent_storage_test_util#meta/util.cmx", |
| std::move(request)); |
| services->Connect(util.NewRequest()); |
| |
| // Verify that the file no longer exists. |
| EXPECT_FALSE(ReadFileSync(util, kTestFileName).has_value()); |
| } |
| |
| using LabelAndValidity = std::tuple<std::string, bool>; |
| class EnvironmentLabelTest : public RealmTest, |
| public ::testing::WithParamInterface<LabelAndValidity> {}; |
| |
| TEST_P(EnvironmentLabelTest, CheckLabelValidity) { |
| // Can't use EnclosingEnvironment here since there's no way to discern between |
| // 'not yet created' and 'failed to create'. This also lets use check the |
| // specific status returned. |
| fuchsia::sys::EnvironmentPtr env; |
| fuchsia::sys::EnvironmentControllerPtr env_controller; |
| |
| zx_status_t env_status = ZX_OK; |
| zx_status_t env_controller_status = ZX_OK; |
| bool env_created = false; |
| env.set_error_handler([&](zx_status_t status) { env_status = status; }); |
| env_controller.set_error_handler([&](zx_status_t status) { env_controller_status = status; }); |
| env_controller.events().OnCreated = [&] { env_created = true; }; |
| |
| auto [label, label_valid] = GetParam(); |
| real_env()->CreateNestedEnvironment(env.NewRequest(), env_controller.NewRequest(), label, |
| /* additional_services = */ nullptr, |
| fuchsia::sys::EnvironmentOptions{}); |
| |
| if (label_valid) { |
| RunLoopUntil([&] { return env_created; }); |
| } else { |
| RunLoopUntil([&] { return env_status == ZX_ERR_INVALID_ARGS; }); |
| RunLoopUntil([&] { return env_controller_status == ZX_ERR_INVALID_ARGS; }); |
| EXPECT_FALSE(env_created); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| InvalidLabels, EnvironmentLabelTest, |
| ::testing::Combine(::testing::Values("", "a/b", "/", ".", "..", "../..", "\t", "\r", "ab\n", |
| std::string("123\0", 4), "\10", "\33", "\177", " ", |
| "my realm", "~", "`", "!", "@", "$", "%", "^", "&", "*", |
| "(", ")", "=", "+", "{", "}", "[", "]", "|", "?", ";", "'", |
| "\"", "<", ">", ",", |
| "fuchsia-pkg://fuchsia.com/abcd#meta/abcd.cmx"), |
| ::testing::Values(false))); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ValidLabels, EnvironmentLabelTest, |
| ::testing::Combine(::testing::Values("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", |
| "0123456789", "#-_:.", "my.realm", "my..realm", |
| "fuchsia-pkg:::fuchsia.com:abcd#meta:abcd.cmx"), |
| ::testing::Values(true))); |
| |
| class RealmFakeLoaderTest : public RealmTest, public fuchsia::sys::Loader { |
| protected: |
| RealmFakeLoaderTest() { |
| sys::testing::EnvironmentServices::ParentOverrides parent_overrides; |
| parent_overrides.loader_service_ = |
| std::make_shared<vfs::Service>([this](zx::channel channel, async_dispatcher_t* dispatcher) { |
| bindings_.AddBinding(this, |
| fidl::InterfaceRequest<fuchsia::sys::Loader>(std::move(channel))); |
| }); |
| enclosing_environment_ = CreateNewEnclosingEnvironment( |
| kRealm, CreateServicesWithParentOverrides(std::move(parent_overrides))); |
| } |
| |
| void LoadUrl(std::string url, LoadUrlCallback callback) override { |
| ASSERT_TRUE(component_url_.empty()); |
| component_url_ = url; |
| } |
| |
| void WaitForComponentLoad() { |
| RunLoopUntil([this] { return !component_url_.empty(); }); |
| } |
| |
| const std::string& component_url() const { return component_url_; } |
| |
| std::unique_ptr<EnclosingEnvironment> enclosing_environment_; |
| |
| private: |
| std::shared_ptr<vfs::Service> loader_service_; |
| fidl::BindingSet<fuchsia::sys::Loader> bindings_; |
| std::string component_url_; |
| }; |
| |
| TEST_F(RealmFakeLoaderTest, CreateInvalidComponent) { |
| TerminationReason reason = TerminationReason::UNKNOWN; |
| int64_t return_code = INT64_MAX; |
| auto controller = RunComponent(enclosing_environment_.get(), "garbage://test"); |
| controller.events().OnTerminated = [&](int64_t err, TerminationReason r) { |
| return_code = err; |
| reason = r; |
| }; |
| RunLoopUntil([&] { return return_code < INT64_MAX; }); |
| EXPECT_EQ(TerminationReason::URL_INVALID, reason); |
| EXPECT_EQ(-1, return_code); |
| } |
| |
| } // namespace |
| } // namespace component |