blob: 7d30e34aa2212f698636d0eecd5d251082083f42 [file] [log] [blame]
// Copyright 2017 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 <dirent.h>
#include <fcntl.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/zx/channel.h>
#include <lib/zx/time.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <fbl/unique_fd.h>
#include "src/storage/fs_test/fs_test_fixture.h"
namespace fs_test {
namespace {
namespace fio = fuchsia_io;
using WatcherTest = FilesystemTest;
struct WatchBuffer {
// Buffer containing cached messages
uint8_t buf[fio::wire::kMaxBuf];
// Offset into 'buf' of next message
uint8_t* ptr;
// Maximum size of buffer
size_t size;
};
// Try to read from the channel when it should be empty.
void CheckForEmpty(WatchBuffer* wb, const fidl::ClientEnd<fio::DirectoryWatcher>& c) {
char name[NAME_MAX + 1];
ASSERT_EQ(wb->ptr, nullptr);
ASSERT_EQ(c.channel().read(0, &name, nullptr, sizeof(name), 0, nullptr, nullptr),
ZX_ERR_SHOULD_WAIT);
}
void CheckLocalEvent(WatchBuffer* wb, const char* expected, fio::wire::WatchEvent event) {
size_t expected_len = strlen(expected);
ASSERT_NE(wb->ptr, nullptr);
// Used a cached event
ASSERT_EQ(static_cast<fio::wire::WatchEvent>(wb->ptr[0]), event);
ASSERT_EQ(wb->ptr[1], expected_len);
ASSERT_EQ(memcmp(wb->ptr + 2, expected, expected_len), 0);
wb->ptr += expected_len + 2;
ASSERT_LE(wb->ptr, wb->buf + wb->size);
if (wb->ptr == wb->buf + wb->size) {
wb->ptr = nullptr;
}
}
// Try to read the 'expected' name off the channel.
void CheckForEvent(WatchBuffer* wb, const fidl::ClientEnd<fio::DirectoryWatcher>& c,
const char* expected, fio::wire::WatchEvent event) {
if (wb->ptr == nullptr) {
zx_signals_t observed;
ASSERT_EQ(c.channel().wait_one(ZX_CHANNEL_READABLE, zx::deadline_after(zx::sec(5)), &observed),
ZX_OK);
ASSERT_EQ(observed & ZX_CHANNEL_READABLE, ZX_CHANNEL_READABLE);
uint32_t actual;
ASSERT_EQ(c.channel().read(0, wb->buf, nullptr, sizeof(wb->buf), 0, &actual, nullptr), ZX_OK);
wb->size = actual;
wb->ptr = wb->buf;
}
CheckLocalEvent(wb, expected, event);
}
TEST_P(WatcherTest, Add) {
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0666), 0);
DIR* dir = opendir(GetPath("dir").c_str());
ASSERT_NE(dir, nullptr);
auto endpoints = fidl::Endpoints<fio::DirectoryWatcher>::Create();
fdio_cpp::FdioCaller caller(fbl::unique_fd(dirfd(dir)));
auto watch_result = fidl::WireCall(caller.borrow_as<fio::Directory>())
->Watch(fio::wire::WatchMask::kAdded, 0, std::move(endpoints.server));
ASSERT_EQ(watch_result.status(), ZX_OK);
ASSERT_EQ(watch_result->s, ZX_OK);
WatchBuffer wb;
memset(&wb, 0, sizeof(wb));
// The channel should be empty
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
// Creating a file in the directory should trigger the watcher
fbl::unique_fd fd(open(GetPath("dir/foo").c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "foo", fio::wire::WatchEvent::kAdded));
// Renaming into directory should trigger the watcher
ASSERT_EQ(rename(GetPath("dir/foo").c_str(), GetPath("dir/bar").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "bar", fio::wire::WatchEvent::kAdded));
if (fs().GetTraits().supports_hard_links) {
// Linking into directory should trigger the watcher
ASSERT_EQ(link(GetPath("dir/bar").c_str(), GetPath("dir/blat").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "blat", fio::wire::WatchEvent::kAdded));
ASSERT_EQ(unlink(GetPath("dir/blat").c_str()), 0);
}
// Clean up
ASSERT_EQ(unlink(GetPath("dir/bar").c_str()), 0) << errno;
// There shouldn't be anything else sitting around on the channel
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
// The fd is still owned by "dir".
caller.release().release();
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir(GetPath("dir").c_str()), 0) << errno;
}
TEST_P(WatcherTest, Existing) {
// This test currently makes assumptions about the order in which entries are returned. For now,
// it creates entries in alphabetical order, which happens to work on filesystems we currently
// support.
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0666), 0);
DIR* dir = opendir(GetPath("dir").c_str());
ASSERT_NE(dir, nullptr);
// Create a couple files in the directory
fbl::unique_fd fd(open(GetPath("dir/bar").c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
fd.reset(open(GetPath("dir/foo").c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
// These files should be visible to the watcher through the "EXISTING"
// mechanism.
auto endpoints = fidl::Endpoints<fio::DirectoryWatcher>::Create();
fdio_cpp::FdioCaller caller(fbl::unique_fd(dirfd(dir)));
fio::wire::WatchMask mask =
fio::wire::WatchMask::kAdded | fio::wire::WatchMask::kExisting | fio::wire::WatchMask::kIdle;
{
auto watch_result = fidl::WireCall(caller.borrow_as<fio::Directory>())
->Watch(mask, 0, std::move(endpoints.server));
ASSERT_EQ(watch_result.status(), ZX_OK);
ASSERT_EQ(watch_result->s, ZX_OK);
}
WatchBuffer wb;
memset(&wb, 0, sizeof(wb));
// The channel should see the contents of the directory
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, ".", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "bar", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "foo", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(CheckForEvent(&wb, endpoints.client, "", fio::wire::WatchEvent::kIdle));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
// Now, if we choose to add additional files, they'll show up separately
// with an "ADD" event.
fd.reset(open(GetPath("dir/goo").c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "goo", fio::wire::WatchEvent::kAdded));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
// If we create a secondary watcher with the "EXISTING" request, we'll
// see all files in the directory, but the first watcher won't see anything.
auto endpoints2 = fidl::Endpoints<fio::DirectoryWatcher>::Create();
{
auto watch_result = fidl::WireCall(caller.borrow_as<fio::Directory>())
->Watch(mask, 0, std::move(endpoints2.server));
ASSERT_EQ(watch_result.status(), ZX_OK);
ASSERT_EQ(watch_result->s, ZX_OK);
}
WatchBuffer wb2;
memset(&wb2, 0, sizeof(wb2));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb2, endpoints2.client, ".", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb2, endpoints2.client, "bar", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb2, endpoints2.client, "foo", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb2, endpoints2.client, "goo", fio::wire::WatchEvent::kExisting));
ASSERT_NO_FATAL_FAILURE(CheckForEvent(&wb2, endpoints2.client, "", fio::wire::WatchEvent::kIdle));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb2, endpoints2.client));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
// Clean up
ASSERT_EQ(unlink(GetPath("dir/bar").c_str()), 0);
ASSERT_EQ(unlink(GetPath("dir/foo").c_str()), 0);
ASSERT_EQ(unlink(GetPath("dir/goo").c_str()), 0);
// There shouldn't be anything else sitting around on either channel
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb2, endpoints2.client));
// The fd is still owned by "dir".
caller.release().release();
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir(GetPath("dir").c_str()), 0);
}
TEST_P(WatcherTest, Removed) {
ASSERT_EQ(mkdir(GetPath("dir").c_str(), 0666), 0);
DIR* dir = opendir(GetPath("dir").c_str());
ASSERT_NE(dir, nullptr);
auto endpoints = fidl::Endpoints<fio::DirectoryWatcher>::Create();
fio::wire::WatchMask mask = fio::wire::WatchMask::kAdded | fio::wire::WatchMask::kRemoved;
fdio_cpp::FdioCaller caller(fbl::unique_fd(dirfd(dir)));
auto watch_result = fidl::WireCall(caller.borrow_as<fio::Directory>())
->Watch(mask, 0, std::move(endpoints.server));
ASSERT_EQ(watch_result.status(), ZX_OK);
ASSERT_EQ(watch_result->s, ZX_OK);
WatchBuffer wb;
memset(&wb, 0, sizeof(wb));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
fbl::unique_fd fd(openat(dirfd(dir), "foo", O_CREAT | O_RDWR | O_EXCL, S_IRUSR | S_IWUSR));
ASSERT_TRUE(fd);
ASSERT_EQ(close(fd.release()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "foo", fio::wire::WatchEvent::kAdded));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
ASSERT_EQ(rename(GetPath("dir/foo").c_str(), GetPath("dir/bar").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "foo", fio::wire::WatchEvent::kRemoved));
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "bar", fio::wire::WatchEvent::kAdded));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
ASSERT_EQ(unlink(GetPath("dir/bar").c_str()), 0);
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, "bar", fio::wire::WatchEvent::kRemoved));
ASSERT_NO_FATAL_FAILURE(CheckForEmpty(&wb, endpoints.client));
// The fd is still owned by "dir".
caller.release().release();
ASSERT_EQ(closedir(dir), 0);
ASSERT_EQ(rmdir(GetPath("dir").c_str()), 0) << errno;
}
TEST_P(WatcherTest, DirectoryDeleted) {
if (!fs().GetTraits().supports_watch_event_deleted) {
std::cout << "Skipping " << fs().GetTraits().name << std::endl;
return;
}
std::string dir_name = GetPath("dir");
ASSERT_EQ(mkdir(dir_name.c_str(), 0666), 0);
DIR* dir = opendir(dir_name.c_str());
ASSERT_NE(dir, nullptr);
{
auto endpoints = fidl::Endpoints<fio::DirectoryWatcher>::Create();
fdio_cpp::FdioCaller caller(fbl::unique_fd(dirfd(dir)));
auto watch_result = fidl::WireCall(caller.borrow_as<fio::Directory>())
->Watch(fio::wire::WatchMask::kDeleted, 0, std::move(endpoints.server));
ASSERT_EQ(watch_result.status(), ZX_OK);
ASSERT_EQ(watch_result->s, ZX_OK);
std::string dir2_name = GetPath("dir2");
ASSERT_EQ(mkdir(dir2_name.c_str(), 0666), 0);
// Renaming over a directory should generate a deleted directory event.
ASSERT_EQ(rename(dir2_name.c_str(), dir_name.c_str()), 0);
WatchBuffer wb = {};
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, ".", fio::wire::WatchEvent::kDeleted));
}
closedir(dir);
dir = opendir(dir_name.c_str());
ASSERT_NE(dir, nullptr);
auto endpoints = fidl::Endpoints<fio::DirectoryWatcher>::Create();
fdio_cpp::FdioCaller caller(fbl::unique_fd(dirfd(dir)));
auto watch_result = fidl::WireCall(caller.borrow_as<fio::Directory>())
->Watch(fio::wire::WatchMask::kDeleted, 0, std::move(endpoints.server));
ASSERT_EQ(watch_result.status(), ZX_OK);
ASSERT_EQ(watch_result->s, ZX_OK);
// Unlinking a directory should generate a deleted directory event.
ASSERT_EQ(rmdir(dir_name.c_str()), 0);
WatchBuffer wb = {};
ASSERT_NO_FATAL_FAILURE(
CheckForEvent(&wb, endpoints.client, ".", fio::wire::WatchEvent::kDeleted));
closedir(dir);
}
INSTANTIATE_TEST_SUITE_P(/*no prefix*/, WatcherTest, testing::ValuesIn(AllTestFilesystems()),
testing::PrintToStringParamName());
} // namespace
} // namespace fs_test