| // 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/syslog/cpp/macros.h> | |
| #include <lib/virtualization/testing/fake_manager.h> | |
| #include <lib/virtualization/testing/guest_cid.h> | |
| #include <threads.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(¶ms); | |
| { // 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 |