diff --git a/source/extensions/common/async_files/async_file_context_thread_pool.cc b/source/extensions/common/async_files/async_file_context_thread_pool.cc index b25e06ca7d0e8..43a07ef051519 100644 --- a/source/extensions/common/async_files/async_file_context_thread_pool.cc +++ b/source/extensions/common/async_files/async_file_context_thread_pool.cc @@ -37,6 +37,22 @@ template class AsyncFileActionThreadPool : public AsyncFileActionWi AsyncFileHandle handle_; }; +class ActionStat : public AsyncFileActionThreadPool> { +public: + ActionStat(AsyncFileHandle handle, std::function)> on_complete) + : AsyncFileActionThreadPool>(handle, on_complete) {} + + absl::StatusOr executeImpl() override { + ASSERT(fileDescriptor() != -1); + struct stat stat_result; + auto result = posix().fstat(fileDescriptor(), &stat_result); + if (result.return_value_ != 0) { + return statusAfterFileError(result); + } + return stat_result; + } +}; + class ActionCreateHardLink : public AsyncFileActionThreadPool { public: ActionCreateHardLink(AsyncFileHandle handle, absl::string_view filename, @@ -171,6 +187,11 @@ class ActionDuplicateFile : public AsyncFileActionThreadPool +AsyncFileContextThreadPool::stat(std::function)> on_complete) { + return checkFileAndEnqueue(std::make_shared(handle(), std::move(on_complete))); +} + absl::StatusOr AsyncFileContextThreadPool::createHardLink(absl::string_view filename, std::function on_complete) { diff --git a/source/extensions/common/async_files/async_file_context_thread_pool.h b/source/extensions/common/async_files/async_file_context_thread_pool.h index 79bdb876f1c28..8c14d75be8212 100644 --- a/source/extensions/common/async_files/async_file_context_thread_pool.h +++ b/source/extensions/common/async_files/async_file_context_thread_pool.h @@ -21,6 +21,8 @@ class AsyncFileContextThreadPool final : public AsyncFileContextBase { public: explicit AsyncFileContextThreadPool(AsyncFileManager& manager, int fd); + absl::StatusOr + stat(std::function)> on_complete) override; absl::StatusOr createHardLink(absl::string_view filename, std::function on_complete) override; diff --git a/source/extensions/common/async_files/async_file_handle.h b/source/extensions/common/async_files/async_file_handle.h index 3d6534733b4e8..a8e54a4166060 100644 --- a/source/extensions/common/async_files/async_file_handle.h +++ b/source/extensions/common/async_files/async_file_handle.h @@ -17,6 +17,10 @@ namespace AsyncFiles { // Instantiated from an AsyncFileManager. class AsyncFileContext : public std::enable_shared_from_this { public: + // Gets a stat struct for the file. + virtual absl::StatusOr + stat(std::function)> on_complete) PURE; + // Action to hard link the file that is currently open. Typically for use in tandem with // createAnonymousFile to turn that file into a named file after finishing writing its contents. // diff --git a/source/extensions/common/async_files/async_file_manager.h b/source/extensions/common/async_files/async_file_manager.h index 1eb0a6a57a69c..d515d49c63b56 100644 --- a/source/extensions/common/async_files/async_file_manager.h +++ b/source/extensions/common/async_files/async_file_manager.h @@ -49,6 +49,14 @@ class AsyncFileManager { openExistingFile(absl::string_view filename, Mode mode, std::function)> on_complete) PURE; + // Action to stat a file. + // on_complete receives a stat structure on success, or an error on failure. + // + // Returns a cancellation function, which aborts the operation + // unless the callback has already been called. + virtual CancelFunction stat(absl::string_view filename, + std::function)> on_complete) PURE; + // Action to delete a named file. // on_complete receives OK on success, or an error on failure. // diff --git a/source/extensions/common/async_files/async_file_manager_thread_pool.cc b/source/extensions/common/async_files/async_file_manager_thread_pool.cc index 8126d8361dcfc..1dfd7b84e4c3e 100644 --- a/source/extensions/common/async_files/async_file_manager_thread_pool.cc +++ b/source/extensions/common/async_files/async_file_manager_thread_pool.cc @@ -214,6 +214,26 @@ class ActionOpenExistingFile : public ActionWithFileResult { const AsyncFileManager::Mode mode_; }; +class ActionStat : public AsyncFileActionWithResult> { +public: + ActionStat(Api::OsSysCalls& posix, absl::string_view filename, + std::function)> on_complete) + : AsyncFileActionWithResult(on_complete), posix_(posix), filename_(filename) {} + + absl::StatusOr executeImpl() override { + struct stat ret; + Api::SysCallIntResult stat_result = posix_.stat(filename_.c_str(), &ret); + if (stat_result.return_value_ == -1) { + return statusAfterFileError(stat_result); + } + return ret; + } + +private: + Api::OsSysCalls& posix_; + const std::string filename_; +}; + class ActionUnlink : public AsyncFileActionWithResult { public: ActionUnlink(Api::OsSysCalls& posix, absl::string_view filename, @@ -246,6 +266,12 @@ CancelFunction AsyncFileManagerThreadPool::openExistingFile( return enqueue(std::make_shared(*this, filename, mode, on_complete)); } +CancelFunction +AsyncFileManagerThreadPool::stat(absl::string_view filename, + std::function)> on_complete) { + return enqueue(std::make_shared(posix(), filename, on_complete)); +} + CancelFunction AsyncFileManagerThreadPool::unlink(absl::string_view filename, std::function on_complete) { return enqueue(std::make_shared(posix(), filename, on_complete)); diff --git a/source/extensions/common/async_files/async_file_manager_thread_pool.h b/source/extensions/common/async_files/async_file_manager_thread_pool.h index 0b2ee24a81ea7..2a82202384337 100644 --- a/source/extensions/common/async_files/async_file_manager_thread_pool.h +++ b/source/extensions/common/async_files/async_file_manager_thread_pool.h @@ -36,6 +36,8 @@ class AsyncFileManagerThreadPool : public AsyncFileManager, CancelFunction openExistingFile(absl::string_view filename, Mode mode, std::function)> on_complete) override; + CancelFunction stat(absl::string_view filename, + std::function)> on_complete) override; CancelFunction unlink(absl::string_view filename, std::function on_complete) override; std::string describe() const override; diff --git a/test/extensions/common/async_files/async_file_handle_thread_pool_test.cc b/test/extensions/common/async_files/async_file_handle_thread_pool_test.cc index 8707ce92afb45..538462fdfa148 100644 --- a/test/extensions/common/async_files/async_file_handle_thread_pool_test.cc +++ b/test/extensions/common/async_files/async_file_handle_thread_pool_test.cc @@ -26,6 +26,7 @@ namespace Common { namespace AsyncFiles { using StatusHelpers::IsOkAndHolds; +using StatusHelpers::StatusIs; using ::testing::_; using ::testing::Eq; using ::testing::Return; @@ -206,7 +207,7 @@ class TestTmpFile { posix.close(fd_); posix.unlink(template_); } - std::string name() { return std::string(template_); } + std::string name() { return {template_}; } private: int fd_; @@ -420,6 +421,36 @@ TEST_F(AsyncFileHandleWithMockPosixTest, CancellingFailedCreateHardLinkInProgres close(handle); } +TEST_F(AsyncFileHandleWithMockPosixTest, StatSuccessReturnsPopulatedStatStruct) { + auto handle = createAnonymousFile(); + struct stat expected_stat = {}; + expected_stat.st_size = 9876; + EXPECT_CALL(mock_posix_file_operations_, fstat(_, _)).WillOnce([&](int, struct stat* buffer) { + *buffer = expected_stat; + return Api::SysCallIntResult{0, 0}; + }); + std::promise> fstat_status_promise; + EXPECT_OK(handle->stat([&](absl::StatusOr status) { + fstat_status_promise.set_value(std::move(status)); + })); + auto fstat_status = fstat_status_promise.get_future().get(); + EXPECT_THAT(fstat_status, IsOkAndHolds(testing::Field(&stat::st_size, expected_stat.st_size))); + close(handle); +} + +TEST_F(AsyncFileHandleWithMockPosixTest, StatFailureReportsError) { + auto handle = createAnonymousFile(); + EXPECT_CALL(mock_posix_file_operations_, fstat(_, _)) + .WillOnce(Return(Api::SysCallIntResult{-1, EBADF})); + std::promise> fstat_status_promise; + EXPECT_OK(handle->stat([&](absl::StatusOr status) { + fstat_status_promise.set_value(std::move(status)); + })); + auto fstat_status = fstat_status_promise.get_future().get(); + EXPECT_THAT(fstat_status, StatusIs(absl::StatusCode::kFailedPrecondition)); + close(handle); +} + TEST_F(AsyncFileHandleWithMockPosixTest, CloseFailureReportsError) { auto handle = createAnonymousFile(); EXPECT_CALL(mock_posix_file_operations_, close(1)) @@ -438,8 +469,7 @@ TEST_F(AsyncFileHandleWithMockPosixTest, DuplicateFailureReportsError) { EXPECT_OK(handle->duplicate( [&](absl::StatusOr status) { dup_status_promise.set_value(status); })); auto dup_status = dup_status_promise.get_future().get(); - EXPECT_EQ(absl::StatusCode::kFailedPrecondition, dup_status.status().code()) - << dup_status.status(); + EXPECT_THAT(dup_status, StatusIs(absl::StatusCode::kFailedPrecondition)); close(handle); } diff --git a/test/extensions/common/async_files/async_file_manager_thread_pool_test.cc b/test/extensions/common/async_files/async_file_manager_thread_pool_test.cc index c7d7d8b74ba19..3ece36519063f 100644 --- a/test/extensions/common/async_files/async_file_manager_thread_pool_test.cc +++ b/test/extensions/common/async_files/async_file_manager_thread_pool_test.cc @@ -25,6 +25,8 @@ namespace Extensions { namespace Common { namespace AsyncFiles { +using StatusHelpers::HasStatusCode; + enum class BlockerState { Start, BlockingDuringExecution, @@ -268,7 +270,7 @@ TEST_F(AsyncFileManagerSingleThreadTest, CreateAnonymousFileWorks) { EXPECT_OK(status); } -TEST_F(AsyncFileManagerSingleThreadTest, OpenExistingFileAndUnlinkWork) { +TEST_F(AsyncFileManagerSingleThreadTest, OpenExistingFileStatAndUnlinkWork) { char filename[1024]; snprintf(filename, sizeof(filename), "%s/async_file.XXXXXX", tmpdir_.c_str()); Api::OsSysCalls& posix = Api::OsSysCallsSingleton().get(); @@ -282,6 +284,11 @@ TEST_F(AsyncFileManagerSingleThreadTest, OpenExistingFileAndUnlinkWork) { EXPECT_OK(handle->close(close_blocker.callback())); absl::Status status = close_blocker.getResult(); EXPECT_OK(status); + WaitForResult> stat_blocker; + manager_->stat(filename, stat_blocker.callback()); + absl::StatusOr stat_result = stat_blocker.getResult(); + EXPECT_OK(stat_result); + EXPECT_EQ(0, stat_result.value().st_size); WaitForResult unlink_blocker; manager_->unlink(filename, unlink_blocker.callback()); status = unlink_blocker.getResult(); @@ -295,14 +302,21 @@ TEST_F(AsyncFileManagerSingleThreadTest, OpenExistingFileFailsForNonexistent) { manager_->openExistingFile(absl::StrCat(tmpdir_, "/nonexistent_file"), AsyncFileManager::Mode::ReadWrite, handle_blocker.callback()); absl::Status status = handle_blocker.getResult().status(); - EXPECT_EQ(absl::StatusCode::kNotFound, status.code()) << status; + EXPECT_THAT(status, HasStatusCode(absl::StatusCode::kNotFound)); +} + +TEST_F(AsyncFileManagerSingleThreadTest, StatFailsForNonexistent) { + WaitForResult> stat_blocker; + manager_->stat(absl::StrCat(tmpdir_, "/nonexistent_file"), stat_blocker.callback()); + absl::StatusOr result = stat_blocker.getResult(); + EXPECT_THAT(result, HasStatusCode(absl::StatusCode::kNotFound)); } TEST_F(AsyncFileManagerSingleThreadTest, UnlinkFailsForNonexistent) { - WaitForResult> handle_blocker; + WaitForResult handle_blocker; manager_->unlink(absl::StrCat(tmpdir_, "/nonexistent_file"), handle_blocker.callback()); - absl::Status status = handle_blocker.getResult().status(); - EXPECT_EQ(absl::StatusCode::kNotFound, status.code()) << status; + absl::Status status = handle_blocker.getResult(); + EXPECT_THAT(status, HasStatusCode(absl::StatusCode::kNotFound)); } } // namespace AsyncFiles diff --git a/test/extensions/common/async_files/mocks.cc b/test/extensions/common/async_files/mocks.cc index 8a782eb663eda..347f61af0aba2 100644 --- a/test/extensions/common/async_files/mocks.cc +++ b/test/extensions/common/async_files/mocks.cc @@ -11,6 +11,11 @@ using ::testing::_; MockAsyncFileContext::MockAsyncFileContext(std::shared_ptr manager) : manager_(manager) { + ON_CALL(*this, stat(_)) + .WillByDefault([this](std::function)> on_complete) { + return manager_->enqueue( + std::shared_ptr(new TypedMockAsyncFileAction(on_complete))); + }); ON_CALL(*this, createHardLink(_, _)) .WillByDefault([this](absl::string_view, std::function on_complete) { return manager_->enqueue( @@ -47,6 +52,12 @@ MockAsyncFileManager::MockAsyncFileManager() { queue_.push_back(std::dynamic_pointer_cast(action)); return [this]() { mockCancel(); }; }); + ON_CALL(*this, stat(_, _)) + .WillByDefault( + [this](absl::string_view, std::function)> on_complete) { + return enqueue( + std::shared_ptr(new TypedMockAsyncFileAction(on_complete))); + }); ON_CALL(*this, createAnonymousFile(_, _)) .WillByDefault([this](absl::string_view, std::function)> on_complete) { diff --git a/test/extensions/common/async_files/mocks.h b/test/extensions/common/async_files/mocks.h index cb2dda1aef9f1..80280f900a85e 100644 --- a/test/extensions/common/async_files/mocks.h +++ b/test/extensions/common/async_files/mocks.h @@ -20,6 +20,8 @@ class MockAsyncFileContext : public Extensions::Common::AsyncFiles::AsyncFileCon // appropriate MockAsyncFileAction on the MockAsyncFileManager (if one was provided). // These can be consumed by calling MockAsyncFileManager::nextActionCompletes // with the desired parameter to the on_complete callback. + MOCK_METHOD(absl::StatusOr, stat, + (std::function)> on_complete)); MOCK_METHOD(absl::StatusOr, createHardLink, (absl::string_view filename, std::function on_complete)); MOCK_METHOD(absl::Status, close, (std::function on_complete)); @@ -61,6 +63,9 @@ class MockAsyncFileManager : public Extensions::Common::AsyncFiles::AsyncFileMan MOCK_METHOD(CancelFunction, openExistingFile, (absl::string_view filename, Mode mode, std::function)> on_complete)); + MOCK_METHOD(CancelFunction, stat, + (absl::string_view filename, + std::function)> on_complete)); MOCK_METHOD(CancelFunction, unlink, (absl::string_view filename, std::function on_complete)); MOCK_METHOD(std::string, describe, (), (const));