| // Copyright 2019 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 <fcntl.h> |
| #include <fuchsia/netemul/guest/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/sys/cpp/file_descriptor.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <unistd.h> |
| |
| #include <fbl/unique_fd.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/testing/predicates/status.h" |
| #include "src/virtualization/lib/guest_interaction/common.h" |
| #include "src/virtualization/lib/guest_interaction/test/integration_test_lib.h" |
| |
| class GuestInteractionTestWithDiscovery : public GuestInteractionTest { |
| protected: |
| GuestInteractionTestWithDiscovery() { |
| services().AddServiceWithLaunchInfo( |
| { |
| .url = kGuestDiscoveryUrl, |
| .out = sys::CloneFileDescriptor(STDOUT_FILENO), |
| .err = sys::CloneFileDescriptor(STDERR_FILENO), |
| |
| }, |
| fuchsia::netemul::guest::GuestDiscovery::Name_); |
| } |
| |
| fuchsia::netemul::guest::GuestInteractionPtr guest_interaction() { |
| if (!guest_discovery_.is_bound()) { |
| env().ConnectToService(guest_discovery_.NewRequest()); |
| } |
| fuchsia::netemul::guest::GuestInteractionPtr guest_interaction; |
| guest_discovery_->GetGuest(std::nullopt, kGuestLabel, guest_interaction.NewRequest()); |
| return guest_interaction; |
| } |
| |
| private: |
| fuchsia::netemul::guest::GuestDiscoveryPtr guest_discovery_; |
| }; |
| |
| // Call into the guest interaction fidl service to push a test bash script, run |
| // the bash script, and pull back the results generated by the script. The |
| // script emits output to stdout and stderr and accepts input from stdin which |
| // is used to generate the output file. |
| TEST_F(GuestInteractionTestWithDiscovery, FidlExecScriptTest) { |
| fuchsia::netemul::guest::GuestInteractionPtr gis = guest_interaction(); |
| std::optional<zx_status_t> gis_status; |
| gis.set_error_handler([&gis_status](zx_status_t status) { gis_status = status; }); |
| |
| // Push the bash script to the guest |
| FX_LOGS(INFO) << "Sending script to guest"; |
| fidl::InterfaceHandle<fuchsia::io::File> put_file; |
| ASSERT_OK(fdio_open(kTestScriptSource, |
| static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE), |
| put_file.NewRequest().TakeChannel().release())); |
| |
| std::optional<zx_status_t> put_status; |
| gis->PutFile(std::move(put_file), kGuestScriptDestination, |
| [&put_status](zx_status_t status) { put_status = status; }); |
| RunLoopUntil( |
| [&gis_status, &put_status]() { return gis_status.has_value() || put_status.has_value(); }); |
| ASSERT_FALSE(gis_status.has_value()) << zx_status_get_string(gis_status.value()); |
| ASSERT_TRUE(put_status.has_value()); |
| ASSERT_OK(put_status.value()); |
| |
| // Run the bash script in the guest. The script will write to stdout and |
| // stderr. The script will also block waiting to receive input from stdin. |
| zx::socket stdin_writer, stdin_reader; |
| ASSERT_OK(zx::socket::create(0, &stdin_writer, &stdin_reader)); |
| |
| zx::socket stdout_writer, stdout_reader; |
| ASSERT_OK(zx::socket::create(0, &stdout_writer, &stdout_reader)); |
| |
| zx::socket stderr_writer, stderr_reader; |
| ASSERT_OK(zx::socket::create(0, &stderr_writer, &stderr_reader)); |
| |
| // Run the bash script on the guest. |
| FX_LOGS(INFO) << "Running script in guest"; |
| std::string command = std::string("/bin/sh "); |
| command.append(kGuestScriptDestination); |
| std::vector<fuchsia::netemul::guest::EnvironmentVariable> env_vars = { |
| {"STDOUT_STRING", kTestStdout}, {"STDERR_STRING", kTestStderr}}; |
| std::string std_out; |
| std::string std_err; |
| std::optional<zx_status_t> exec_started; |
| std::optional<zx_status_t> exec_terminated; |
| |
| fuchsia::netemul::guest::CommandListenerPtr listener; |
| listener.events().OnStarted = [&](zx_status_t status) { |
| // Once the subprocess has started, write to stdin. |
| std::string to_write = std::string(::kTestScriptInput); |
| uint32_t bytes_written = 0; |
| |
| while (bytes_written < to_write.size()) { |
| size_t curr_bytes_written; |
| zx_status_t write_status = |
| stdin_writer.write(0, &(to_write.c_str()[bytes_written]), to_write.size() - bytes_written, |
| &curr_bytes_written); |
| if (write_status != ZX_OK) { |
| return; |
| } |
| |
| bytes_written += curr_bytes_written; |
| } |
| stdin_writer.reset(); |
| exec_started = status; |
| }; |
| listener.events().OnTerminated = [&](zx_status_t status, int32_t exit_code) { |
| // When the process terminates, read from stdout and stderr. |
| exec_terminated = status; |
| |
| size_t bytes_read = 0; |
| char read_buf[100]; |
| while (true) { |
| zx_status_t read_status = stdout_reader.read(0, read_buf, 100, &bytes_read); |
| if (read_status != ZX_OK) { |
| break; |
| } |
| std_out += std::string(read_buf, bytes_read); |
| } |
| while (true) { |
| zx_status_t read_status = stderr_reader.read(0, read_buf, 100, &bytes_read); |
| if (read_status != ZX_OK) { |
| break; |
| } |
| std_err += std::string(read_buf, bytes_read); |
| } |
| }; |
| |
| gis->ExecuteCommand(command, env_vars, std::move(stdin_reader), std::move(stdout_writer), |
| std::move(stderr_writer), listener.NewRequest()); |
| |
| // Wait for the subprocess to start. |
| RunLoopUntil([&gis_status, &exec_started]() { |
| return gis_status.has_value() || exec_started.has_value(); |
| }); |
| ASSERT_FALSE(gis_status.has_value()) << zx_status_get_string(gis_status.value()); |
| ASSERT_TRUE(exec_started.has_value()); |
| ASSERT_OK(exec_started.value()); |
| |
| // Wait for the bash script to finish. |
| RunLoopUntil([&gis_status, &exec_terminated]() { |
| return gis_status.has_value() || exec_terminated.has_value(); |
| }); |
| ASSERT_FALSE(gis_status.has_value()) << zx_status_get_string(gis_status.value()); |
| ASSERT_TRUE(exec_terminated.has_value()); |
| ASSERT_OK(exec_terminated.value()); |
| |
| // Validate the stdout and stderr. |
| ASSERT_EQ(std_out, (std::string(kTestStdout) + std::string("\n"))); |
| ASSERT_EQ(std_err, (std::string(kTestStderr) + std::string("\n"))); |
| |
| // The bash script will create a file with contents that were written to |
| // stdin. Pull this file back and inspect its contents. |
| fidl::InterfaceHandle<fuchsia::io::File> get_file; |
| ASSERT_OK(fdio_open( |
| kHostOuputCopyLocation, |
| static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_WRITABLE | |
| fuchsia::io::OpenFlags::CREATE | fuchsia::io::OpenFlags::TRUNCATE), |
| get_file.NewRequest().TakeChannel().release())); |
| |
| std::optional<zx_status_t> get_status; |
| gis->GetFile(kGuestFileOutputLocation, std::move(get_file), |
| [&get_status](zx_status_t status) { get_status = status; }); |
| RunLoopUntil( |
| [&gis_status, &get_status]() { return gis_status.has_value() || get_status.has_value(); }); |
| ASSERT_FALSE(gis_status.has_value()) << zx_status_get_string(gis_status.value()); |
| ASSERT_TRUE(get_status.has_value()); |
| ASSERT_OK(get_status.value()); |
| |
| // Verify the contents that were communicated through stdin. |
| std::string output_string; |
| { |
| fbl::unique_fd fd; |
| ASSERT_TRUE(fd = fbl::unique_fd(open(kHostOuputCopyLocation, O_RDONLY))) << strerror(errno); |
| |
| char output_buf[100]; |
| while (true) { |
| ssize_t read_size = read(fd.get(), output_buf, sizeof(output_buf)); |
| ASSERT_GE(read_size, 0) << strerror(errno); |
| if (read_size == 0) { |
| break; |
| } |
| output_string.append(std::string(output_buf, read_size)); |
| } |
| } |
| |
| ASSERT_EQ(output_string, std::string(kTestScriptInput)); |
| } |
| |
| TEST_F(GuestInteractionTestWithDiscovery, FidlPutGetTest) { |
| fuchsia::netemul::guest::GuestInteractionPtr gis = guest_interaction(); |
| |
| // Write a file of gibberish that the test can send over to the guest. |
| constexpr char test_file[] = "/tmp/test_file.txt"; |
| constexpr char guest_destination[] = "/root/new/directory/test_file.txt"; |
| constexpr char host_verification_file[] = "/tmp/verification_file.txt"; |
| |
| std::string file_contents; |
| for (int i = 0; i < 2 * CHUNK_SIZE; i++) { |
| file_contents.push_back(static_cast<char>(i % (('z' - 'A') + 'A'))); |
| } |
| { |
| fbl::unique_fd fd; |
| ASSERT_TRUE(fd = fbl::unique_fd(open(test_file, O_WRONLY | O_TRUNC | O_CREAT))) |
| << strerror(errno); |
| uint32_t bytes_written = 0; |
| while (bytes_written < file_contents.size()) { |
| ssize_t write_size = write(fd.get(), file_contents.c_str() + bytes_written, |
| file_contents.size() - bytes_written); |
| ASSERT_GT(write_size, 0); |
| bytes_written += write_size; |
| } |
| } |
| |
| // Push the file to the guest |
| fidl::InterfaceHandle<fuchsia::io::File> put_file; |
| ASSERT_OK(fdio_open(test_file, static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE), |
| put_file.NewRequest().TakeChannel().release())); |
| |
| std::optional<zx_status_t> status; |
| gis.set_error_handler([&status](zx_status_t error_status) { status = error_status; }); |
| gis->PutFile(std::move(put_file), guest_destination, |
| [&status](zx_status_t put_result) { status = put_result; }); |
| RunLoopUntil([&status]() { return status.has_value(); }); |
| ASSERT_TRUE(status.has_value()); |
| ASSERT_OK(status.value()); |
| |
| // Pull back the guest's copy of the file and ensure the contents match those |
| // from the file generated above. |
| fidl::InterfaceHandle<fuchsia::io::File> get_file; |
| ASSERT_OK(fdio_open( |
| host_verification_file, |
| static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_WRITABLE | |
| fuchsia::io::OpenFlags::CREATE | fuchsia::io::OpenFlags::TRUNCATE), |
| get_file.NewRequest().TakeChannel().release())); |
| |
| status.reset(); |
| gis->GetFile(guest_destination, std::move(get_file), |
| [&status](zx_status_t get_result) { status = get_result; }); |
| RunLoopUntil([&status]() { return status.has_value(); }); |
| ASSERT_TRUE(status.has_value()); |
| ASSERT_OK(status.value()); |
| |
| // Verify the contents that were communicated through stdin. |
| std::string output_string; |
| { |
| fbl::unique_fd fd; |
| ASSERT_TRUE(fd = fbl::unique_fd(open(host_verification_file, O_RDONLY))) << strerror(errno); |
| |
| char output_buf[100]; |
| while (true) { |
| ssize_t read_size = read(fd.get(), output_buf, sizeof(output_buf)); |
| ASSERT_GE(read_size, 0) << strerror(errno); |
| if (read_size == 0) { |
| break; |
| } |
| output_string.append(std::string(output_buf, read_size)); |
| } |
| } |
| |
| ASSERT_EQ(output_string, file_contents); |
| } |