| // 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 <fuchsia/io/llcpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/fdio.h> |
| #include <stdio.h> |
| |
| #include <atomic> |
| #include <utility> |
| |
| #include <fs/pseudo_dir.h> |
| #include <fs/pseudo_file.h> |
| #include <fs/synchronous_vfs.h> |
| #include <zxtest/zxtest.h> |
| |
| namespace { |
| |
| namespace fio = ::llcpp::fuchsia::io; |
| |
| zx_status_t DummyReader(fbl::String* output) { return ZX_OK; } |
| |
| zx_status_t DummyWriter(fbl::StringPiece input) { return ZX_OK; } |
| |
| // Example vnode that supports protocol negotiation. |
| // Here the vnode may be opened as a file or a directory. |
| class FileOrDirectory : public fs::Vnode { |
| public: |
| FileOrDirectory() = default; |
| |
| fs::VnodeProtocolSet GetProtocols() const final { |
| return fs::VnodeProtocol::kFile | fs::VnodeProtocol::kDirectory; |
| } |
| |
| zx_status_t GetNodeInfoForProtocol(fs::VnodeProtocol protocol, fs::Rights, |
| fs::VnodeRepresentation* info) final { |
| switch (protocol) { |
| case fs::VnodeProtocol::kFile: |
| *info = fs::VnodeRepresentation::File(); |
| break; |
| case fs::VnodeProtocol::kDirectory: |
| *info = fs::VnodeRepresentation::Directory(); |
| break; |
| default: |
| ZX_ASSERT_MSG(false, "Unreachable"); |
| } |
| return ZX_OK; |
| } |
| }; |
| |
| class VfsTestSetup : public zxtest::Test { |
| public: |
| // Setup file structure with one directory and one file. Note: On creation |
| // directories and files have no flags and rights. |
| VfsTestSetup() : loop_(&kAsyncLoopConfigNoAttachToCurrentThread) { |
| vfs_.SetDispatcher(loop_.dispatcher()); |
| root_ = fbl::AdoptRef<fs::PseudoDir>(new fs::PseudoDir()); |
| dir_ = fbl::AdoptRef<fs::PseudoDir>(new fs::PseudoDir()); |
| file_ = fbl::AdoptRef<fs::Vnode>(new fs::BufferedPseudoFile(&DummyReader, &DummyWriter)); |
| file_or_dir_ = fbl::AdoptRef<FileOrDirectory>(new FileOrDirectory()); |
| root_->AddEntry("dir", dir_); |
| root_->AddEntry("file", file_); |
| root_->AddEntry("file_or_dir", file_or_dir_); |
| } |
| |
| zx_status_t ConnectClient(zx::channel server_end) { |
| // Serve root directory with maximum rights |
| return vfs_.ServeDirectory(root_, std::move(server_end)); |
| } |
| |
| protected: |
| void SetUp() override { loop_.StartThread(); } |
| |
| void TearDown() override { loop_.Shutdown(); } |
| |
| private: |
| async::Loop loop_; |
| fs::SynchronousVfs vfs_; |
| fbl::RefPtr<fs::PseudoDir> root_; |
| fbl::RefPtr<fs::PseudoDir> dir_; |
| fbl::RefPtr<fs::Vnode> file_; |
| fbl::RefPtr<FileOrDirectory> file_or_dir_; |
| }; |
| |
| using ConnectionTest = VfsTestSetup; |
| |
| TEST_F(ConnectionTest, NodeGetSetFlagsOnFile) { |
| // Create connection to vfs |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| // Connect to File |
| zx::channel fc1, fc2; |
| ASSERT_OK(zx::channel::create(0u, &fc1, &fc2)); |
| ASSERT_OK(fdio_open_at(client_end.get(), "file", fio::OPEN_RIGHT_READABLE, fc2.release())); |
| |
| // Use NodeGetFlags to get current flags and rights |
| auto file_get_result = fio::Node::Call::NodeGetFlags(zx::unowned_channel(fc1)); |
| EXPECT_OK(file_get_result.status()); |
| EXPECT_EQ(fio::OPEN_RIGHT_READABLE, file_get_result.Unwrap()->flags); |
| { |
| // Make modifications to flags with NodeSetFlags: Note this only works for OPEN_FLAG_APPEND |
| // based on posix standard |
| auto file_set_result = |
| fio::Node::Call::NodeSetFlags(zx::unowned_channel(fc1), fio::OPEN_FLAG_APPEND); |
| EXPECT_OK(file_set_result.Unwrap()->s); |
| } |
| { |
| // Check that the new flag is saved |
| auto file_get_result = fio::Node::Call::NodeGetFlags(zx::unowned_channel(fc1)); |
| EXPECT_OK(file_get_result.Unwrap()->s); |
| EXPECT_EQ(fio::OPEN_RIGHT_READABLE | fio::OPEN_FLAG_APPEND, file_get_result.Unwrap()->flags); |
| } |
| } |
| |
| TEST_F(ConnectionTest, NodeGetSetFlagsOnDirectory) { |
| // Create connection to vfs |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| // Connect to Directory |
| zx::channel dc1, dc2; |
| ASSERT_OK(zx::channel::create(0u, &dc1, &dc2)); |
| ASSERT_OK(fdio_open_at(client_end.get(), "dir", |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE, dc2.release())); |
| |
| // Read/write/read directory flags; same as for file |
| auto dir_get_result = fio::Node::Call::NodeGetFlags(zx::unowned_channel(dc1)); |
| EXPECT_OK(dir_get_result.Unwrap()->s); |
| EXPECT_EQ(fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE, dir_get_result.Unwrap()->flags); |
| |
| auto dir_set_result = |
| fio::Node::Call::NodeSetFlags(zx::unowned_channel(dc1), fio::OPEN_FLAG_APPEND); |
| EXPECT_OK(dir_set_result.Unwrap()->s); |
| |
| auto dir_get_result_2 = fio::Node::Call::NodeGetFlags(zx::unowned_channel(dc1)); |
| EXPECT_OK(dir_get_result_2.Unwrap()->s); |
| EXPECT_EQ(fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE | fio::OPEN_FLAG_APPEND, |
| dir_get_result_2.Unwrap()->flags); |
| } |
| |
| TEST_F(ConnectionTest, FileGetSetFlagsOnFile) { |
| // Create connection to vfs |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| // Connect to File |
| zx::channel fc1, fc2; |
| ASSERT_OK(zx::channel::create(0u, &fc1, &fc2)); |
| ASSERT_OK(fdio_open_at(client_end.get(), "file", fio::OPEN_RIGHT_READABLE, fc2.release())); |
| |
| { |
| // Use NodeGetFlags to get current flags and rights |
| auto file_get_result = fio::File::Call::GetFlags(zx::unowned_channel(fc1)); |
| EXPECT_OK(file_get_result.status()); |
| EXPECT_EQ(fio::OPEN_RIGHT_READABLE, file_get_result.Unwrap()->flags); |
| } |
| { |
| // Make modifications to flags with NodeSetFlags: Note this only works for OPEN_FLAG_APPEND |
| // based on posix standard |
| auto file_set_result = |
| fio::File::Call::SetFlags(zx::unowned_channel(fc1), fio::OPEN_FLAG_APPEND); |
| EXPECT_OK(file_set_result.Unwrap()->s); |
| } |
| { |
| // Check that the new flag is saved |
| auto file_get_result = fio::File::Call::GetFlags(zx::unowned_channel(fc1)); |
| EXPECT_OK(file_get_result.Unwrap()->s); |
| EXPECT_EQ(fio::OPEN_RIGHT_READABLE | fio::OPEN_FLAG_APPEND, file_get_result.Unwrap()->flags); |
| } |
| } |
| |
| TEST_F(ConnectionTest, FileGetSetFlagsDirectory) { |
| // Create connection to vfs |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| // Read/write flags on a Directory connection using File protocol Get/SetFlags should fail. |
| { |
| zx::channel dc1, dc2; |
| ASSERT_OK(zx::channel::create(0u, &dc1, &dc2)); |
| ASSERT_OK(fdio_open_at(client_end.get(), "dir", |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE, dc2.release())); |
| |
| auto dir_get_result = fio::File::Call::GetFlags(zx::unowned_channel(dc1)); |
| EXPECT_NOT_OK(dir_get_result.status()); |
| } |
| |
| { |
| zx::channel dc1, dc2; |
| ASSERT_OK(zx::channel::create(0u, &dc1, &dc2)); |
| ASSERT_OK(fdio_open_at(client_end.get(), "dir", |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_WRITABLE, dc2.release())); |
| |
| auto dir_set_result = |
| fio::File::Call::SetFlags(zx::unowned_channel(dc1), fio::OPEN_FLAG_APPEND); |
| EXPECT_NOT_OK(dir_set_result.status()); |
| } |
| } |
| |
| TEST_F(ConnectionTest, NegotiateProtocol) { |
| // Create connection to vfs |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| // Helper method to monitor the OnOpen event, used by the tests below |
| auto expect_on_open = [](zx::unowned_channel channel, fit::function<void(fio::NodeInfo)> cb) { |
| zx_status_t event_status = fio::Node::Call::HandleEvents( |
| std::move(channel), |
| fio::Node::EventHandlers{.on_open = |
| [&](zx_status_t status, fio::NodeInfo info) { |
| EXPECT_OK(status); |
| EXPECT_FALSE(info.has_invalid_tag()); |
| cb(std::move(info)); |
| return ZX_OK; |
| }, |
| .unknown = []() { return ZX_ERR_INVALID_ARGS; }}); |
| // Expect that |on_open| was received |
| EXPECT_EQ(ZX_OK, event_status); |
| }; |
| |
| constexpr uint32_t kOpenMode = 0755; |
| |
| // Connect to polymorphic node as a directory, by passing |OPEN_FLAG_DIRECTORY|. |
| zx::channel dc1, dc2; |
| ASSERT_OK(zx::channel::create(0u, &dc1, &dc2)); |
| ASSERT_OK(fio::Directory::Call::Open( |
| zx::unowned_channel(client_end), |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_FLAG_DESCRIBE | fio::OPEN_FLAG_DIRECTORY, |
| kOpenMode, fidl::StringView("file_or_dir"), std::move(dc2)) |
| .status()); |
| expect_on_open(zx::unowned_channel(dc1), |
| [](fio::NodeInfo info) { EXPECT_TRUE(info.is_directory()); }); |
| |
| // Connect to polymorphic node as a file, by passing |OPEN_FLAG_NOT_DIRECTORY|. |
| zx::channel fc1, fc2; |
| ASSERT_OK(zx::channel::create(0u, &fc1, &fc2)); |
| ASSERT_OK(fio::Directory::Call::Open( |
| zx::unowned_channel(client_end), |
| fio::OPEN_RIGHT_READABLE | fio::OPEN_FLAG_DESCRIBE | fio::OPEN_FLAG_NOT_DIRECTORY, |
| kOpenMode, fidl::StringView("file_or_dir"), std::move(fc2)) |
| .status()); |
| expect_on_open(zx::unowned_channel(fc1), [](fio::NodeInfo info) { EXPECT_TRUE(info.is_file()); }); |
| } |
| |
| // A vnode which maintains a counter of number of |Open| calls that |
| // have not been balanced out with a |Close|. |
| class CountOutstandingOpenVnode : public fs::Vnode { |
| public: |
| CountOutstandingOpenVnode() = default; |
| |
| fs::VnodeProtocolSet GetProtocols() const final { return fs::VnodeProtocol::kFile; } |
| |
| zx_status_t GetNodeInfoForProtocol(fs::VnodeProtocol protocol, fs::Rights rights, |
| fs::VnodeRepresentation* info) final { |
| *info = fs::VnodeRepresentation::File{}; |
| return ZX_OK; |
| } |
| |
| zx_status_t Open(ValidatedOptions options, fbl::RefPtr<Vnode>* out_redirect) final { |
| num_open_.fetch_add(1); |
| return ZX_OK; |
| } |
| |
| zx_status_t Close() final { |
| num_open_.fetch_sub(1); |
| return ZX_OK; |
| } |
| |
| uint64_t num_open() const { return num_open_.load(); } |
| |
| private: |
| std::atomic<uint64_t> num_open_ = {}; |
| }; |
| |
| class ConnectionClosingTest : public zxtest::Test { |
| public: |
| // Setup file structure with one directory and one file. Note: On creation |
| // directories and files have no flags and rights. |
| ConnectionClosingTest() : loop_(&kAsyncLoopConfigNoAttachToCurrentThread) { |
| vfs_.SetDispatcher(loop_.dispatcher()); |
| root_ = fbl::AdoptRef<fs::PseudoDir>(new fs::PseudoDir()); |
| count_outstanding_open_vnode_ = fbl::AdoptRef(new CountOutstandingOpenVnode()); |
| root_->AddEntry("count_outstanding_open_vnode", count_outstanding_open_vnode_); |
| } |
| |
| zx_status_t ConnectClient(zx::channel server_end) { |
| // Serve root directory with maximum rights |
| return vfs_.ServeDirectory(root_, std::move(server_end)); |
| } |
| |
| protected: |
| fbl::RefPtr<CountOutstandingOpenVnode> count_outstanding_open_vnode() const { |
| return count_outstanding_open_vnode_; |
| } |
| |
| async::Loop& loop() { return loop_; } |
| |
| private: |
| async::Loop loop_; |
| fs::SynchronousVfs vfs_; |
| fbl::RefPtr<fs::PseudoDir> root_; |
| fbl::RefPtr<CountOutstandingOpenVnode> count_outstanding_open_vnode_; |
| }; |
| |
| TEST_F(ConnectionClosingTest, ClosingChannelImpliesClosingNode) { |
| // Create connection to vfs. |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| constexpr uint32_t kOpenMode = 0755; |
| constexpr int kNumActiveClients = 20; |
| |
| ASSERT_EQ(count_outstanding_open_vnode()->num_open(), 0); |
| |
| // Create a number of active connections to "count_outstanding_open_vnode". |
| std::vector<zx::channel> clients; |
| for (int i = 0; i < kNumActiveClients; i++) { |
| zx::channel fc1, fc2; |
| ASSERT_OK(zx::channel::create(0u, &fc1, &fc2)); |
| ASSERT_OK(fio::Directory::Call::Open( |
| zx::unowned_channel(client_end), fio::OPEN_RIGHT_READABLE, kOpenMode, |
| fidl::StringView("count_outstanding_open_vnode"), std::move(fc2)) |
| .status()); |
| clients.push_back(std::move(fc1)); |
| } |
| |
| ASSERT_OK(loop().RunUntilIdle()); |
| ASSERT_EQ(count_outstanding_open_vnode()->num_open(), kNumActiveClients); |
| |
| // Drop all the clients, leading to |Close| being invoked |
| // on "count_outstanding_open_vnode" eventually. |
| clients.clear(); |
| |
| ASSERT_OK(loop().RunUntilIdle()); |
| ASSERT_EQ(count_outstanding_open_vnode()->num_open(), 0); |
| } |
| |
| TEST_F(ConnectionClosingTest, ClosingNodeLeadsToClosingServerEndChannel) { |
| // Create connection to vfs. |
| zx::channel client_end, server_end; |
| ASSERT_OK(zx::channel::create(0u, &client_end, &server_end)); |
| ASSERT_OK(ConnectClient(std::move(server_end))); |
| |
| zx_signals_t observed = ZX_SIGNAL_NONE; |
| ASSERT_STATUS(ZX_ERR_TIMED_OUT, |
| client_end.wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time::infinite_past(), &observed)); |
| ASSERT_FALSE(observed & ZX_CHANNEL_PEER_CLOSED); |
| |
| ASSERT_OK(loop().StartThread()); |
| auto result = fio::Node::Call::Close(zx::unowned_channel(client_end)); |
| ASSERT_OK(result.status()); |
| ASSERT_OK(result->s); |
| |
| observed = ZX_SIGNAL_NONE; |
| ASSERT_OK(client_end.wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time::infinite(), &observed)); |
| ASSERT_TRUE(observed & ZX_CHANNEL_PEER_CLOSED); |
| |
| loop().Shutdown(); |
| } |
| |
| } // namespace |