blob: 30adae672a55c44db37f7e1022ddbd5dfb10ebe2 [file] [log] [blame]
// 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);
}