blob: 57ff872c0de3e1e4072f5cdd3762856f25229ae3 [file] [log] [blame]
// 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/sys/cpp/file_descriptor.h>
#include <threads.h>
#include <map>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
#include <src/virtualization/tests/guest_console.h>
#include "src/lib/fxl/strings/trim.h"
#include "src/lib/testing/predicates/status.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"
namespace {
using ::fuchsia::virtualization::HostVsockEndpoint_Connect2_Result;
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 void ConvertSocketToNonBlockingFd(zx::socket socket, fbl::unique_fd& fd) {
ASSERT_OK(fdio_fd_create(socket.release(), fd.reset_and_get_address()));
int result;
ASSERT_EQ((result = SetNonBlocking(fd)), 0) << strerror(result);
}
TEST_F(GuestInteractionTest, GrpcExecScriptTest) {
// Connect the gRPC client to the guest under test.
fuchsia::virtualization::HostVsockEndpointPtr ep;
realm()->GetHostVsockEndpoint(ep.NewRequest());
zx::socket local_socket;
std::optional<zx_status_t> status;
auto callback = [&status, &local_socket](HostVsockEndpoint_Connect2_Result result) {
if (result.is_response()) {
local_socket = std::move(result.response().socket);
status = ZX_OK;
} else {
status = result.err();
}
};
ep->Connect2(GUEST_INTERACTION_PORT, std::move(callback));
RunLoopUntil([&status]() { return status.has_value(); });
ASSERT_OK(status.value());
fbl::unique_fd vsock_fd;
ASSERT_NO_FATAL_FAILURE(ConvertSocketToNonBlockingFd(std::move(local_socket), vsock_fd));
ClientImpl<PosixPlatform> client(vsock_fd.release());
// Push the bash script to the guest.
fidl::InterfaceHandle<fuchsia::io::File> put_remote;
ASSERT_OK(fdio_open(kTestScriptSource,
static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE),
put_remote.NewRequest().TakeChannel().release()));
status.reset();
client.Put(std::move(put_remote), kGuestScriptDestination,
[&client, &status](zx_status_t put_result) {
status = put_result;
client.Stop();
});
client.Run();
ASSERT_TRUE(status.has_value());
ASSERT_OK(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));
// Once the subprocess has started, write to stdin.
std::string to_write = kTestScriptInput;
uint32_t bytes_written = 0;
fbl::unique_fd stdin_fd;
ASSERT_NO_FATAL_FAILURE(ConvertSocketToNonBlockingFd(std::move(stdin_writer), stdin_fd));
// 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;
std::optional<zx_status_t> exec_started;
std::optional<zx_status_t> exec_terminated;
int32_t ret_code = -1;
fuchsia::netemul::guest::CommandListenerPtr listener;
listener.events().OnStarted = [&](zx_status_t status) {
exec_started = status;
if (status == ZX_OK) {
while (bytes_written < to_write.size()) {
size_t curr_bytes_written = write(stdin_fd.get(), &(to_write.c_str()[bytes_written]),
to_write.size() - bytes_written);
if (curr_bytes_written < 0) {
break;
}
bytes_written += curr_bytes_written;
}
}
client.Stop();
stdin_fd.reset();
};
listener.events().OnTerminated = [&](zx_status_t exec_result, int32_t exit_code) {
exec_terminated = exec_result;
ret_code = exit_code;
std_out = drain_socket(std::move(stdout_reader));
std_err = drain_socket(std::move(stderr_reader));
client.Stop();
};
client.Exec(command, env_vars, std::move(stdin_reader), std::move(stdout_writer),
std::move(stderr_writer), listener.NewRequest(), dispatcher());
// Ensure that the process started cleanly.
{
thrd_t client_run_thread;
ASSERT_EQ(client.Start(client_run_thread), thrd_success);
RunLoopUntil([&exec_started] { return exec_started.has_value(); });
int32_t thread_ret_code;
ASSERT_EQ(thrd_join(client_run_thread, &thread_ret_code), thrd_success);
ASSERT_EQ(thread_ret_code, 0);
}
ASSERT_TRUE(exec_started.has_value());
ASSERT_OK(exec_started.value());
// Ensure the gRPC operation completed successfully and validate the stdout
// and stderr.
{
thrd_t client_run_thread;
ASSERT_EQ(client.Start(client_run_thread), thrd_success);
RunLoopUntil([&exec_terminated] { return exec_terminated.has_value(); });
int32_t thread_ret_code;
ASSERT_EQ(thrd_join(client_run_thread, &thread_ret_code), thrd_success);
ASSERT_EQ(thread_ret_code, 0);
}
ASSERT_TRUE(exec_terminated.has_value());
ASSERT_OK(exec_terminated.value());
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.
fidl::InterfaceHandle<fuchsia::io::File> get_remote;
ASSERT_OK(fdio_open(
kHostOuputCopyLocation,
static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_WRITABLE |
fuchsia::io::OpenFlags::CREATE | fuchsia::io::OpenFlags::TRUNCATE),
get_remote.NewRequest().TakeChannel().release()));
status.reset();
client.Get(kGuestFileOutputLocation, std::move(get_remote), [&](zx_status_t get_result) {
status = get_result;
client.Stop();
});
client.Run();
ASSERT_TRUE(status.has_value());
ASSERT_OK(status.value());
// Verify the contents that were communicated through stdin.
std::string output_string;
char output_buf[kBufferSize];
fbl::unique_fd fd;
ASSERT_TRUE(fd = fbl::unique_fd(open(kHostOuputCopyLocation, O_RDONLY))) << strerror(errno);
while (true) {
ssize_t read_size = read(fd.get(), output_buf, kBufferSize);
ASSERT_GE(read_size, 0) << strerror(errno);
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.
TEST_F(GuestInteractionTest, GrpcPutGetTest) {
// Connect the gRPC client to the guest under test.
fuchsia::virtualization::HostVsockEndpointPtr ep;
realm()->GetHostVsockEndpoint(ep.NewRequest());
zx::socket local_socket;
std::optional<zx_status_t> status;
auto callback = [&status, &local_socket](HostVsockEndpoint_Connect2_Result result) {
if (result.is_response()) {
local_socket = std::move(result.response().socket);
status = ZX_OK;
} else {
status = result.err();
}
};
ep->Connect2(GUEST_INTERACTION_PORT, std::move(callback));
RunLoopUntil([&status] { return status.has_value(); });
ASSERT_TRUE(status.has_value());
ASSERT_OK(status.value());
fbl::unique_fd vsock_fd;
ASSERT_NO_FATAL_FAILURE(ConvertSocketToNonBlockingFd(std::move(local_socket), vsock_fd));
ClientImpl<PosixPlatform> client(vsock_fd.release());
// 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 test file to the guest
fidl::InterfaceHandle<fuchsia::io::File> put_remote;
ASSERT_OK(fdio_open(test_file, static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE),
put_remote.NewRequest().TakeChannel().release()));
status.reset();
client.Put(std::move(put_remote), guest_destination, [&](zx_status_t put_result) {
status = put_result;
client.Stop();
});
client.Run();
ASSERT_TRUE(status.has_value());
ASSERT_OK(status.value());
// Copy back the file that was sent to the guest.
fidl::InterfaceHandle<fuchsia::io::File> get_remote;
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_remote.NewRequest().TakeChannel().release()));
status.reset();
client.Get(guest_destination, std::move(get_remote), [&](zx_status_t get_result) {
status = get_result;
client.Stop();
});
client.Run();
ASSERT_TRUE(status.has_value());
ASSERT_OK(status.value());
// 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) {
ssize_t read_size = read(fd.get(), output_buf, kBufferSize);
ASSERT_GE(read_size, 0) << strerror(errno);
if (read_size == 0) {
break;
}
verification_string.append(std::string(output_buf, read_size));
}
ASSERT_EQ(verification_string, file_contents);
}
} // namespace