| // 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 <fcntl.h> |
| #include <fuchsia/virtualization/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/vfs.h> |
| #include <lib/sys/cpp/file_descriptor.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <threads.h> |
| |
| #include <map> |
| |
| #include <fbl/unique_fd.h> |
| #include <gtest/gtest.h> |
| #include <src/virtualization/tests/fake_netstack.h> |
| #include <src/virtualization/tests/guest_console.h> |
| |
| #include "src/lib/fxl/strings/trim.h" |
| #include "src/virtualization/lib/grpc/fdio_util.h" |
| #include "src/virtualization/lib/guest_interaction/client/client_impl.h" |
| #include "src/virtualization/lib/guest_interaction/common.h" |
| #include "src/virtualization/lib/guest_interaction/test/integration_test_lib.h" |
| |
| static constexpr size_t kBufferSize = 100; |
| |
| std::string drain_socket(zx::socket socket) { |
| std::string out_string; |
| size_t bytes_read; |
| char read_buf[kBufferSize]; |
| |
| while (true) { |
| zx_status_t read_status = socket.read(0, read_buf, kBufferSize, &bytes_read); |
| if (read_status == ZX_ERR_SHOULD_WAIT) { |
| continue; |
| } |
| if (read_status != ZX_OK) { |
| break; |
| } |
| out_string.append(read_buf, bytes_read); |
| } |
| |
| return out_string; |
| } |
| |
| static int run_grpc_client(void* client_to_run) { |
| ClientImpl<PosixPlatform>* client_impl = (ClientImpl<PosixPlatform>*)client_to_run; |
| client_impl->Run(); |
| return 0; |
| } |
| |
| TEST_F(GuestInteractionTest, GrpcExecScriptTest) { |
| CreateEnvironment(); |
| LaunchDebianGuest(); |
| |
| // Connect the gRPC client to the guest under test. |
| fuchsia::virtualization::HostVsockEndpointPtr ep; |
| realm_->GetHostVsockEndpoint(ep.NewRequest()); |
| |
| zx::socket local_socket, remote_socket; |
| zx_status_t status = zx::socket::create(ZX_SOCKET_STREAM, &local_socket, &remote_socket); |
| ASSERT_EQ(status, ZX_OK); |
| |
| bool connect_complete = false; |
| ep->Connect(cid_, GUEST_INTERACTION_PORT, std::move(remote_socket), |
| [&connect_complete, &status](zx_status_t connect_status) { |
| status = connect_status; |
| connect_complete = true; |
| }); |
| RunLoopUntil([&connect_complete] { return connect_complete; }); |
| ASSERT_EQ(status, ZX_OK); |
| |
| int32_t vsock_fd = ConvertSocketToNonBlockingFd(std::move(local_socket)); |
| ASSERT_GE(vsock_fd, 0); |
| |
| ClientImpl<PosixPlatform> client(vsock_fd); |
| |
| // Push the bash script to the guest |
| zx::channel put_local, put_remote; |
| zx::channel::create(0, &put_local, &put_remote); |
| zx_status_t open_status = |
| fdio_open(kTestScriptSource, fuchsia::io::OPEN_RIGHT_READABLE, put_local.release()); |
| ASSERT_EQ(open_status, ZX_OK); |
| |
| zx_status_t transfer_status = ZX_ERR_IO; |
| client.Put(std::move(put_remote), kGuestScriptDestination, [&](zx_status_t put_result) { |
| transfer_status = put_result; |
| client.Stop(); |
| }); |
| client.Run(); |
| ASSERT_EQ(transfer_status, ZX_OK); |
| |
| // 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; |
| zx::socket::create(0, &stdin_writer, &stdin_reader); |
| |
| zx::socket stdout_writer, stdout_reader; |
| zx::socket::create(0, &stdout_writer, &stdout_reader); |
| |
| zx::socket stderr_writer, stderr_reader; |
| zx::socket::create(0, &stderr_writer, &stderr_reader); |
| |
| // Once the subprocess has started, write to stdin. |
| std::string to_write = kTestScriptInput; |
| uint32_t bytes_written = 0; |
| int32_t stdin_fd = ConvertSocketToNonBlockingFd(std::move(stdin_writer)); |
| |
| // Run the bash script on the guest. |
| std::string command = "/bin/sh "; |
| command.append(kGuestScriptDestination); |
| std::map<std::string, std::string> env_vars{{"STDOUT_STRING", kTestStdout}, |
| {"STDERR_STRING", kTestStderr}}; |
| std::string std_out; |
| std::string std_err; |
| zx_status_t exec_started_status = ZX_ERR_PEER_CLOSED; |
| zx_status_t exec_terminated_status = ZX_ERR_PEER_CLOSED; |
| bool exec_started = false; |
| bool exec_terminated = false; |
| int32_t ret_code = -1; |
| |
| fuchsia::netemul::guest::CommandListenerPtr listener; |
| listener.events().OnStarted = [&](zx_status_t status) { |
| exec_started_status = status; |
| if (status == ZX_OK) { |
| while (bytes_written < to_write.size()) { |
| size_t curr_bytes_written = |
| write(stdin_fd, &(to_write.c_str()[bytes_written]), to_write.size() - bytes_written); |
| if (curr_bytes_written < 0) { |
| break; |
| } |
| |
| bytes_written += curr_bytes_written; |
| } |
| } |
| client.Stop(); |
| close(stdin_fd); |
| exec_started = true; |
| }; |
| listener.events().OnTerminated = [&](zx_status_t exec_result, int32_t exit_code) { |
| exec_terminated_status = exec_result; |
| ret_code = exit_code; |
| |
| std_out = drain_socket(std::move(stdout_reader)); |
| std_err = drain_socket(std::move(stderr_reader)); |
| client.Stop(); |
| exec_terminated = true; |
| }; |
| |
| client.Exec(command, env_vars, std::move(stdin_reader), std::move(stdout_writer), |
| std::move(stderr_writer), listener.NewRequest()); |
| |
| // Ensure that the process started cleanly. |
| thrd_t client_run_thread; |
| thrd_create_with_name(&client_run_thread, run_grpc_client, &client, "gRPC run"); |
| RunLoopUntil([&exec_started] { return exec_started; }); |
| int32_t thread_ret_code; |
| thrd_join(client_run_thread, &thread_ret_code); |
| |
| ASSERT_EQ(exec_started_status, ZX_OK); |
| |
| // Ensure the gRPC operation completed successfully and validate the stdout |
| // and stderr. |
| thrd_create_with_name(&client_run_thread, run_grpc_client, &client, "gRPC run"); |
| RunLoopUntil([&exec_terminated] { return exec_terminated; }); |
| thrd_join(client_run_thread, &thread_ret_code); |
| |
| ASSERT_EQ(exec_terminated_status, ZX_OK); |
| ASSERT_EQ(fxl::TrimString(std_out, "\n"), std::string_view(kTestStdout)); |
| ASSERT_EQ(fxl::TrimString(std_err, "\n"), std::string_view(kTestStderr)); |
| |
| // The bash script will create a file with contents that were written to |
| // stdin. Pull this file back and inspect its contents. |
| zx::channel get_local, get_remote; |
| zx::channel::create(0, &get_local, &get_remote); |
| open_status = fdio_open(kHostOuputCopyLocation, |
| fuchsia::io::OPEN_RIGHT_WRITABLE | fuchsia::io::OPEN_FLAG_CREATE | |
| fuchsia::io::OPEN_FLAG_TRUNCATE, |
| get_local.release()); |
| ASSERT_EQ(open_status, ZX_OK); |
| |
| transfer_status = ZX_ERR_IO; |
| client.Get(kGuestFileOutputLocation, std::move(get_remote), [&](zx_status_t get_result) { |
| transfer_status = get_result; |
| client.Stop(); |
| }); |
| client.Run(); |
| ASSERT_EQ(transfer_status, ZX_OK); |
| |
| // Verify the contents that were communicated through stdin. |
| std::string output_string; |
| char output_buf[kBufferSize]; |
| fbl::unique_fd fd = fbl::unique_fd(open(kHostOuputCopyLocation, O_RDONLY)); |
| while (true) { |
| size_t read_size = read(fd.get(), output_buf, kBufferSize); |
| if (read_size <= 0) |
| break; |
| output_string.append(output_buf, read_size); |
| } |
| |
| ASSERT_STREQ(output_string.c_str(), kTestScriptInput); |
| } |
| |
| // Create a file large enough that it needs to be fragmented when it is sent |
| // to and received from the guest and then send it to and retrieve it from the |
| // guest. |
| // TODO(fxbug.dev/43569): Flaky. |
| TEST_F(GuestInteractionTest, DISABLED_GrpcPutGetTest) { |
| CreateEnvironment(); |
| LaunchDebianGuest(); |
| |
| // Connect the gRPC client to the guest under test. |
| fuchsia::virtualization::HostVsockEndpointPtr ep; |
| realm_->GetHostVsockEndpoint(ep.NewRequest()); |
| |
| zx::socket local_socket, remote_socket; |
| zx_status_t status = zx::socket::create(ZX_SOCKET_STREAM, &local_socket, &remote_socket); |
| ASSERT_EQ(status, ZX_OK); |
| |
| bool connect_complete = false; |
| ep->Connect(cid_, GUEST_INTERACTION_PORT, std::move(remote_socket), |
| [&connect_complete, &status](zx_status_t connect_status) { |
| status = connect_status; |
| connect_complete = true; |
| }); |
| RunLoopUntil([&connect_complete] { return connect_complete; }); |
| ASSERT_EQ(status, ZX_OK); |
| |
| int32_t vsock_fd = ConvertSocketToNonBlockingFd(std::move(local_socket)); |
| ASSERT_GE(vsock_fd, 0); |
| |
| ClientImpl<PosixPlatform> client(vsock_fd); |
| |
| // Write a file of gibberish that the test can send over to the guest. |
| char test_file[] = "/data/test_file.txt"; |
| char guest_destination[] = "/root/new/directory/test_file.txt"; |
| char host_verification_file[] = "/data/verification_file.txt"; |
| |
| std::string file_contents; |
| for (int i = 0; i < 2 * CHUNK_SIZE; i++) { |
| file_contents.push_back(i % ('z' - 'A') + 'A'); |
| } |
| fbl::unique_fd fd = fbl::unique_fd(open(test_file, O_WRONLY | O_TRUNC | O_CREAT)); |
| 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_TRUE(write_size > 0); |
| bytes_written += write_size; |
| } |
| |
| // Push the test file to the guest |
| zx::channel put_local, put_remote; |
| zx::channel::create(0, &put_local, &put_remote); |
| zx_status_t open_status = |
| fdio_open(test_file, fuchsia::io::OPEN_RIGHT_READABLE, put_local.release()); |
| ASSERT_EQ(open_status, ZX_OK); |
| |
| zx_status_t transfer_status = ZX_ERR_IO; |
| client.Put(std::move(put_remote), guest_destination, [&](zx_status_t put_result) { |
| transfer_status = put_result; |
| client.Stop(); |
| }); |
| client.Run(); |
| ASSERT_EQ(transfer_status, ZX_OK); |
| |
| // Copy back the file that was sent to the guest. |
| zx::channel get_local, get_remote; |
| zx::channel::create(0, &get_local, &get_remote); |
| open_status = fdio_open(host_verification_file, |
| fuchsia::io::OPEN_RIGHT_WRITABLE | fuchsia::io::OPEN_FLAG_CREATE | |
| fuchsia::io::OPEN_FLAG_TRUNCATE, |
| get_local.release()); |
| ASSERT_EQ(open_status, ZX_OK); |
| |
| transfer_status = ZX_ERR_IO; |
| client.Get(guest_destination, std::move(get_remote), [&](zx_status_t get_result) { |
| transfer_status = get_result; |
| client.Stop(); |
| }); |
| client.Run(); |
| ASSERT_EQ(transfer_status, OperationStatus::OK); |
| |
| // Verify the contents that were communicated through stdin. |
| std::string verification_string; |
| char output_buf[kBufferSize]; |
| fd.reset(open(host_verification_file, O_RDONLY)); |
| while (true) { |
| size_t read_size = read(fd.get(), output_buf, kBufferSize); |
| if (read_size <= 0) |
| break; |
| verification_string.append(std::string(output_buf, read_size)); |
| } |
| |
| ASSERT_EQ(verification_string, file_contents); |
| } |