blob: 231ce8e2bd3650e5fc51ab917bf97bca11de0462 [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 "src/virtualization/lib/vsh/command_runner.h"
#include <lib/async/cpp/executor.h>
#include <lib/gtest/test_loop_fixture.h>
#include <lib/sys/cpp/testing/component_context_provider.h>
#include <lib/virtualization/testing/fake_manager.h>
#include <lib/virtualization/testing/guest_cid.h>
#include <threads.h>
#include "src/lib/fxl/logging.h"
#include "src/virtualization/lib/vsh/util.h"
#include "src/virtualization/packages/biscotti_guest/third_party/protos/vsh.pb.h"
namespace vsh {
namespace {
// FakeVshd simply listens on the guest vsock endpoint and accepts connections
// on the vshd port.
//
// Tests can access the accepted connections with |TakeConnection| and then
// simulate vshd by reading/writing messages to the socket.
struct FakeVshd {
zx_status_t Listen(guest::testing::FakeGuestVsock* guest_vsock) {
return guest_vsock->Listen(vsh::kVshPort, fit::bind_member(this, &FakeVshd::OnNewConnection));
}
zx::socket TakeConnection() {
FX_CHECK(!connections.empty());
auto conn = std::move(connections[0]);
connections.erase(connections.begin());
return conn;
}
std::vector<zx::socket> connections;
private:
zx_status_t OnNewConnection(zx::handle h) {
zx::socket socket(std::move(h));
connections.push_back(std::move(socket));
return ZX_OK;
}
};
struct BlockingCommandRunnerParams {
BlockingCommandRunner runner;
BlockingCommandRunner::Command command;
fit::result<BlockingCommandRunner::CommandResult, zx_status_t> result;
};
int RunBlockingCommand(void* arg) {
BlockingCommandRunnerParams* params = static_cast<BlockingCommandRunnerParams*>(arg);
params->result = params->runner.Execute(std::move(params->command));
return 0;
}
class VshCommandRunnerTest : public gtest::TestLoopFixture {
public:
void SetUp() override {
TestLoopFixture::SetUp();
// Setup a fake guest realm that we can use to connect to a
// HostVsockEndpoint.
provider_.service_directory_provider()->AddService(fake_guest_manager_.GetHandler());
fuchsia::virtualization::ManagerPtr manager;
provider_.context()->svc()->Connect(manager.NewRequest());
manager->Create("test realm", realm_.NewRequest());
RunLoopUntilIdle();
ASSERT_EQ(ZX_OK, vshd_.Listen(fake_guest_manager_.GuestVsock()));
}
void TearDown() override { CloseConnectionsAndWaitForThreadJoin(); }
protected:
void SendStdout(const zx::socket& socket, const std::string& data) {
SendStdio(socket, data, vm_tools::vsh::StdioStream::STDOUT_STREAM);
}
void SendStderr(const zx::socket& socket, const std::string& data) {
SendStdio(socket, data, vm_tools::vsh::StdioStream::STDERR_STREAM);
}
void SendStdio(const zx::socket& socket, std::string data, vm_tools::vsh::StdioStream stream) {
vm_tools::vsh::HostMessage host_message;
auto data_message = host_message.mutable_data_message();
data_message->set_stream(stream);
data_message->set_data(data);
ASSERT_TRUE(vsh::SendMessage(socket, host_message));
}
void SendExit(const zx::socket& socket, int return_code) {
vm_tools::vsh::HostMessage host_message;
auto status_message = host_message.mutable_status_message();
status_message->set_status(vm_tools::vsh::ConnectionStatus::EXITED);
status_message->set_code(return_code);
ASSERT_TRUE(vsh::SendMessage(socket, host_message));
}
// Connects to the |HostVsockEnpoint| for the realm.
fidl::InterfaceHandle<fuchsia::virtualization::HostVsockEndpoint> GetHostVsockEndpoint() {
fuchsia::virtualization::HostVsockEndpointPtr result;
realm_->GetHostVsockEndpoint(result.NewRequest());
RunLoopUntilIdle();
return result;
}
// Starts a new thread to execute the |BlockingCommandRunner| on. This is on
// a different thread so that we can continue to interact with the
// |BlockingCommandRunner| on the main thread by interacting with the server
// end of the vshd socket.
//
// It is an error to call this method multiple times before a corresponding
// call to |CloseConnectionsAndWaitForThreadJoin|.
zx::socket StartBlockingCommandOnThread(BlockingCommandRunnerParams* params) {
FX_CHECK(!thread_running_);
FX_CHECK(thrd_create(&thread_, RunBlockingCommand, params) == thrd_success);
thread_running_ = true;
// Ensure the command runner is connected before continuing.
size_t count = 0;
while (vshd_.connections.empty() && count++ < 1000) {
usleep(1000);
RunLoopUntilIdle();
}
FX_CHECK(!vshd_.connections.empty());
return vshd_.TakeConnection();
}
// If the thread is running a |BlockingCommandRunner| wait for the thread
// to join.
//
// Note this also closes all sockets that have been accepted. This is to
// enable the |BlockingCommandRunner| to become unblocked from any blocking
// read/write operations by virtue of receiving a |ZX_SOCKET_PEER_CLOSED|
// signal.
void CloseConnectionsAndWaitForThreadJoin() {
vshd_.connections.clear();
if (thread_running_) {
thrd_join(thread_, nullptr);
thread_running_ = false;
}
}
private:
bool thread_running_ = false;
thrd_t thread_;
FakeVshd vshd_;
fuchsia::virtualization::RealmPtr realm_;
guest::testing::FakeManager fake_guest_manager_;
sys::testing::ComponentContextProvider provider_;
std::unique_ptr<BlockingCommandRunner> command_runner_;
};
// Test sending and receiving messages by simulating a simple vsh session.
TEST_F(VshCommandRunnerTest, RunCommandCollectResult) {
BlockingCommandRunnerParams params{
{GetHostVsockEndpoint(), guest::testing::kGuestCid, vsh::kVshPort},
/* argv */ /*env*/
{{"some_test_exe"}, {}},
{},
};
// Run the blocking operation on a different thread. Returns a socket to the
// server (vshd) endpoint of the session.
zx::socket conn = StartBlockingCommandOnThread(&params);
{ // Read SetupConnectionRequest
vm_tools::vsh::SetupConnectionRequest request;
ASSERT_TRUE(vsh::RecvMessage(conn, &request));
ASSERT_EQ(0, request.env_size());
ASSERT_EQ(1, request.argv_size());
ASSERT_EQ("some_test_exe", request.argv(0));
}
{ // Send SetupConnectionResponse
vm_tools::vsh::SetupConnectionResponse response;
response.set_status(vm_tools::vsh::ConnectionStatus::READY);
ASSERT_TRUE(vsh::SendMessage(conn, response));
}
// Send some stdout/stderr
SendStdout(conn, "Hello ");
SendStderr(conn, "WARNING:");
SendStdout(conn, "world!");
SendStderr(conn, " this isn't real");
// Now terminate the application
SendExit(conn, -123);
CloseConnectionsAndWaitForThreadJoin();
// Verify we collected the expected out/err/exit_code.
ASSERT_TRUE(params.result.is_ok());
auto command_result = params.result.take_value();
ASSERT_EQ(-123, command_result.return_code);
ASSERT_EQ("Hello world!", command_result.out);
ASSERT_EQ("WARNING: this isn't real", command_result.err);
}
} // namespace
} // namespace vsh