| // Copyright 2018 The Chromium 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/engine/context_provider_impl.h" |
| |
| #include <fuchsia/web/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <zircon/processargs.h> |
| #include <zircon/types.h> |
| |
| #include <functional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base_paths_fuchsia.h" |
| #include "base/base_switches.h" |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/callback.h" |
| #include "base/command_line.h" |
| #include "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/fuchsia/default_job.h" |
| #include "base/fuchsia/file_utils.h" |
| #include "base/fuchsia/fuchsia_logging.h" |
| #include "base/fuchsia/service_directory.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/path_service.h" |
| #include "base/test/multiprocess_test.h" |
| #include "base/test/test_timeouts.h" |
| #include "fuchsia/engine/common.h" |
| #include "fuchsia/engine/fake_context.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/multiprocess_func_list.h" |
| |
| namespace { |
| |
| constexpr char kTestDataFileIn[] = "DataFileIn"; |
| constexpr char kTestDataFileOut[] = "DataFileOut"; |
| constexpr char kUrl[] = "chrome://:emorhc"; |
| constexpr char kTitle[] = "Palindrome"; |
| |
| MULTIPROCESS_TEST_MAIN(SpawnContextServer) { |
| base::MessageLoopForIO message_loop; |
| |
| base::FilePath data_dir; |
| CHECK(base::PathService::Get(base::DIR_APP_DATA, &data_dir)); |
| if (!data_dir.empty()) { |
| if (base::PathExists(data_dir.AppendASCII(kTestDataFileIn))) { |
| auto out_file = data_dir.AppendASCII(kTestDataFileOut); |
| EXPECT_EQ(base::WriteFile(out_file, nullptr, 0), 0); |
| } |
| } |
| |
| fidl::InterfaceRequest<fuchsia::web::Context> fuchsia_context( |
| zx::channel(zx_take_startup_handle(kContextRequestHandleId))); |
| CHECK(fuchsia_context); |
| |
| FakeContext context; |
| fidl::Binding<fuchsia::web::Context> context_binding( |
| &context, std::move(fuchsia_context)); |
| |
| // When a Frame's NavigationEventListener is bound, immediately broadcast a |
| // navigation event to its listeners. |
| context.set_on_create_frame_callback( |
| base::BindRepeating([](FakeFrame* frame) { |
| frame->set_on_set_listener_callback(base::BindOnce( |
| [](FakeFrame* frame) { |
| fuchsia::web::NavigationState state; |
| state.set_url(kUrl); |
| state.set_title(kTitle); |
| frame->listener()->OnNavigationStateChanged(std::move(state), |
| []() {}); |
| }, |
| frame)); |
| })); |
| |
| // Quit the process when the context is destroyed. |
| base::RunLoop run_loop; |
| context_binding.set_error_handler([&run_loop](zx_status_t status) { |
| EXPECT_EQ(status, ZX_ERR_PEER_CLOSED); |
| run_loop.Quit(); |
| }); |
| run_loop.Run(); |
| |
| return 0; |
| } |
| |
| base::Process LaunchFakeContextProcess(const base::CommandLine& command_line, |
| const base::LaunchOptions& options) { |
| base::LaunchOptions options_with_tmp = options; |
| options_with_tmp.paths_to_clone.push_back(base::FilePath("/tmp")); |
| return base::SpawnMultiProcessTestChild("SpawnContextServer", command_line, |
| options_with_tmp); |
| } |
| |
| } // namespace |
| |
| class ContextProviderImplTest : public base::MultiProcessTest { |
| public: |
| ContextProviderImplTest() |
| : provider_(std::make_unique<ContextProviderImpl>()) { |
| provider_->SetLaunchCallbackForTest( |
| base::BindRepeating(&LaunchFakeContextProcess)); |
| bindings_.AddBinding(provider_.get(), provider_ptr_.NewRequest()); |
| } |
| |
| ~ContextProviderImplTest() override { |
| provider_ptr_.Unbind(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Check if a Context is responsive by creating a Frame from it and then |
| // listening for an event. |
| void CheckContextResponsive( |
| fidl::InterfacePtr<fuchsia::web::Context>* context) { |
| // Call a Context method and wait for it to invoke a listener call. |
| base::RunLoop run_loop; |
| context->set_error_handler([&run_loop](zx_status_t status) { |
| ZX_LOG(ERROR, status) << " Context lost."; |
| ADD_FAILURE(); |
| run_loop.Quit(); |
| }); |
| |
| fuchsia::web::FramePtr frame_ptr; |
| frame_ptr.set_error_handler([&run_loop](zx_status_t status) { |
| ZX_LOG(ERROR, status) << " Frame lost."; |
| ADD_FAILURE(); |
| run_loop.Quit(); |
| }); |
| (*context)->CreateFrame(frame_ptr.NewRequest()); |
| |
| // Create a Frame and expect to see a navigation event. |
| CapturingNavigationStateObserver change_listener(run_loop.QuitClosure()); |
| fidl::Binding<fuchsia::web::NavigationEventListener> |
| change_listener_binding(&change_listener); |
| frame_ptr->SetNavigationEventListener(change_listener_binding.NewBinding()); |
| run_loop.Run(); |
| |
| ASSERT_TRUE(change_listener.captured_state()->has_url()); |
| EXPECT_EQ(change_listener.captured_state()->url(), kUrl); |
| ASSERT_TRUE(change_listener.captured_state()->has_title()); |
| EXPECT_EQ(change_listener.captured_state()->title(), kTitle); |
| } |
| |
| fuchsia::web::CreateContextParams BuildCreateContextParams() { |
| fidl::InterfaceHandle<fuchsia::io::Directory> directory; |
| zx_status_t result = |
| fdio_service_connect(base::fuchsia::kServiceDirectoryPath, |
| directory.NewRequest().TakeChannel().release()); |
| ZX_CHECK(result == ZX_OK, result) << "Failed to open /svc"; |
| |
| fuchsia::web::CreateContextParams output; |
| output.set_service_directory(std::move(directory)); |
| return output; |
| } |
| |
| // Checks that the Context channel was dropped. |
| void CheckContextUnresponsive( |
| fidl::InterfacePtr<fuchsia::web::Context>* context) { |
| base::RunLoop run_loop; |
| context->set_error_handler([&run_loop](zx_status_t status) { |
| EXPECT_EQ(status, ZX_ERR_PEER_CLOSED); |
| run_loop.Quit(); |
| }); |
| |
| fuchsia::web::FramePtr frame; |
| (*context)->CreateFrame(frame.NewRequest()); |
| |
| // The error handler should be called here. |
| run_loop.Run(); |
| } |
| |
| protected: |
| base::MessageLoopForIO message_loop_; |
| std::unique_ptr<ContextProviderImpl> provider_; |
| fuchsia::web::ContextProviderPtr provider_ptr_; |
| fidl::BindingSet<fuchsia::web::ContextProvider> bindings_; |
| |
| private: |
| struct CapturingNavigationStateObserver |
| : public fuchsia::web::NavigationEventListener { |
| public: |
| explicit CapturingNavigationStateObserver(base::OnceClosure on_change_cb) |
| : on_change_cb_(std::move(on_change_cb)) {} |
| ~CapturingNavigationStateObserver() override = default; |
| |
| void OnNavigationStateChanged( |
| fuchsia::web::NavigationState change, |
| OnNavigationStateChangedCallback callback) override { |
| captured_state_ = std::move(change); |
| std::move(on_change_cb_).Run(); |
| } |
| |
| fuchsia::web::NavigationState* captured_state() { return &captured_state_; } |
| |
| private: |
| base::OnceClosure on_change_cb_; |
| fuchsia::web::NavigationState captured_state_; |
| }; |
| |
| DISALLOW_COPY_AND_ASSIGN(ContextProviderImplTest); |
| }; |
| |
| TEST_F(ContextProviderImplTest, LaunchContext) { |
| // Connect to a new context process. |
| fidl::InterfacePtr<fuchsia::web::Context> context; |
| fuchsia::web::CreateContextParams create_params = BuildCreateContextParams(); |
| provider_ptr_->Create(std::move(create_params), context.NewRequest()); |
| CheckContextResponsive(&context); |
| } |
| |
| TEST_F(ContextProviderImplTest, MultipleConcurrentClients) { |
| // Bind a Provider connection, and create a Context from it. |
| fuchsia::web::ContextProviderPtr provider_1_ptr; |
| bindings_.AddBinding(provider_.get(), provider_1_ptr.NewRequest()); |
| fuchsia::web::ContextPtr context_1; |
| provider_1_ptr->Create(BuildCreateContextParams(), context_1.NewRequest()); |
| |
| // Do the same on another Provider connection. |
| fuchsia::web::ContextProviderPtr provider_2_ptr; |
| bindings_.AddBinding(provider_.get(), provider_2_ptr.NewRequest()); |
| fuchsia::web::ContextPtr context_2; |
| provider_2_ptr->Create(BuildCreateContextParams(), context_2.NewRequest()); |
| |
| CheckContextResponsive(&context_1); |
| CheckContextResponsive(&context_2); |
| |
| // Ensure that the initial ContextProvider connection is still usable, by |
| // creating and verifying another Context from it. |
| fuchsia::web::ContextPtr context_3; |
| provider_2_ptr->Create(BuildCreateContextParams(), context_3.NewRequest()); |
| CheckContextResponsive(&context_3); |
| } |
| |
| TEST_F(ContextProviderImplTest, WithProfileDir) { |
| base::ScopedTempDir profile_temp_dir; |
| |
| // Connect to a new context process. |
| fidl::InterfacePtr<fuchsia::web::Context> context; |
| fuchsia::web::CreateContextParams create_params = BuildCreateContextParams(); |
| |
| // Setup data dir. |
| EXPECT_TRUE(profile_temp_dir.CreateUniqueTempDir()); |
| ASSERT_EQ( |
| base::WriteFile(profile_temp_dir.GetPath().AppendASCII(kTestDataFileIn), |
| nullptr, 0), |
| 0); |
| |
| // Pass a handle data dir to the context. |
| create_params.set_data_directory( |
| base::fuchsia::OpenDirectory(profile_temp_dir.GetPath())); |
| |
| provider_ptr_->Create(std::move(create_params), context.NewRequest()); |
| |
| CheckContextResponsive(&context); |
| |
| // Verify that the context process can write to the data dir. |
| EXPECT_TRUE(base::PathExists( |
| profile_temp_dir.GetPath().AppendASCII(kTestDataFileOut))); |
| } |
| |
| TEST_F(ContextProviderImplTest, FailsDataDirectoryIsFile) { |
| base::FilePath temp_file_path; |
| |
| // Connect to a new context process. |
| fidl::InterfacePtr<fuchsia::web::Context> context; |
| fuchsia::web::CreateContextParams create_params = BuildCreateContextParams(); |
| |
| // Pass in a handle to a file instead of a directory. |
| CHECK(base::CreateTemporaryFile(&temp_file_path)); |
| create_params.set_data_directory( |
| base::fuchsia::OpenDirectory(temp_file_path)); |
| |
| provider_ptr_->Create(std::move(create_params), context.NewRequest()); |
| |
| CheckContextUnresponsive(&context); |
| } |
| |
| static bool WaitUntilJobIsEmpty(zx::unowned_job job, zx::duration timeout) { |
| zx_signals_t observed = 0; |
| zx_status_t status = |
| job->wait_one(ZX_JOB_NO_JOBS, zx::deadline_after(timeout), &observed); |
| ZX_CHECK(status == ZX_OK || status == ZX_ERR_TIMED_OUT, status); |
| return observed & ZX_JOB_NO_JOBS; |
| } |
| |
| // Regression test for https://crbug.com/927403 (Job leak per-Context). |
| TEST_F(ContextProviderImplTest, CleansUpContextJobs) { |
| // Replace the default job with one that is guaranteed to be empty. |
| zx::job job; |
| ASSERT_EQ(base::GetDefaultJob()->duplicate(ZX_RIGHT_SAME_RIGHTS, &job), |
| ZX_OK); |
| base::ScopedDefaultJobForTest empty_default_job(std::move(job)); |
| |
| // Bind to the ContextProvider. |
| fuchsia::web::ContextProviderPtr provider; |
| bindings_.AddBinding(provider_.get(), provider.NewRequest()); |
| |
| // Verify that our current default job is still empty. |
| ASSERT_TRUE(WaitUntilJobIsEmpty(base::GetDefaultJob(), zx::duration())); |
| |
| // Create a Context and verify that it is functional. |
| fuchsia::web::ContextPtr context; |
| provider->Create(BuildCreateContextParams(), context.NewRequest()); |
| CheckContextResponsive(&context); |
| |
| // Verify that there is at least one job under our default job. |
| ASSERT_FALSE(WaitUntilJobIsEmpty(base::GetDefaultJob(), zx::duration())); |
| |
| // Detach from the Context and ContextProvider, and spin the loop to allow |
| // those to be handled. |
| context.Unbind(); |
| provider.Unbind(); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Wait until the default job signals that it no longer contains any child |
| // jobs; this should occur shortly after the Context process terminates. |
| EXPECT_TRUE(WaitUntilJobIsEmpty( |
| base::GetDefaultJob(), |
| zx::duration(TestTimeouts::action_timeout().InNanoseconds()))); |
| } |