Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion score/launch_manager/src/daemon/src/osal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")

cc_library(
name = "return_types",
Expand Down Expand Up @@ -109,6 +109,36 @@ cc_library(
],
)

cc_library(
name = "inotify_range",
hdrs = [
"inotify_range.hpp",
],
include_prefix = "score/mw/launch_manager/osal",
strip_include_prefix = "/score/launch_manager/src/daemon/src/osal",
visibility = [
"//score:__subpackages__",
"//tests:__subpackages__",
],
deps = [
Comment thread
MaciejKaszynski marked this conversation as resolved.
"@score_baselibs//score/language/futurecpp",
"@score_baselibs//score/os/utils/inotify:inotify_event",
"@score_baselibs//score/os/utils/inotify:inotify_instance",
],
)

cc_test(
name = "inotify_range_UT",
srcs = [
"inotify_range_UT.cpp",
],
deps = [
":inotify_range",
"@googletest//:gtest_main",
"@score_baselibs//score/os/utils/inotify:inotify_instance_mock",
],
)

cc_library(
name = "osal",
visibility = ["//score:__subpackages__"],
Expand Down
177 changes: 177 additions & 0 deletions score/launch_manager/src/daemon/src/osal/inotify_range.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

#ifndef SCORE_MW_LIFECYCLE_OSAL_INOTIFY_RANGE_HPP_
#define SCORE_MW_LIFECYCLE_OSAL_INOTIFY_RANGE_HPP_

#include <cstddef>
#include <iterator>
#include <memory>
#include <utility>

#include "score/assert.hpp"
#include "score/os/utils/inotify/inotify_event.h"
#include "score/os/utils/inotify/inotify_instance.h"
#include "score/static_vector.hpp"

Comment thread
MaciejKaszynski marked this conversation as resolved.
namespace score::mw::lifecycle
Comment thread
MaciejKaszynski marked this conversation as resolved.
{

/// @brief A wrapper around score::os::InotifyInstance to get iterator syntax.
class INotifyRange
{
public:
explicit INotifyRange(std::unique_ptr<score::os::InotifyInstance> instance) noexcept

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I completely understand how this would be used.
It looks like you would still need to interact with the InotifyInstance to setup the file notification first.
And then later to call Close() but the pointer has already been moved to the INotifyRange

E.g. within some kind of FileWatchReadyCondition class:

std::unique_ptr<InotifyInstance> inotify = std::make_unique<InotifyInstanceImpl>();

// init
bool init() {
  if(!inotify->IsValid) {
    // ready condition failed
     return false;
  }
  
  if(!inotify->AddWatch(path, event_mask)) {
    // ready condition failed
    return false;
  }
}

// at runtime
score::cpp::expected_blank<Error> WaitForReadyCondition(stop_token) {
   INotifyRange range(std::move(inotify);
   while(!ready_timeout_over) {
       for(events : range) {  // does this block? How to interrupt this on timeout?
          // if the expected event occured -> return Success
          // else continue until timeout
       }
   }
}

// at destruction
// want to call inotify->Close() but its already moved out

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add this to a FileHandler class that will work like OsHandler (seperate thread).

I will add a stop() method I think then this would be the code

FileHandler::run(){
    auto instance = std::make_unique<score::os::InotifyInstanceImpl>();
    instance.AddWatch("/tmp", score::os::Inotify::EventMask::kInCreate);
    range_ = INotifyRange{std::move(instance)}
    for (const score::os::InotifyEvent& event : range_)
    {
        std::cout << event.GetName() << std::endl;
    };
}

FileHandler::stop()
{
    range_.stop();
}

: instance_(std::move(instance))
{
}

~INotifyRange() = default;

INotifyRange(INotifyRange&& other) noexcept = default;
INotifyRange& operator=(INotifyRange&& other) noexcept = default;

INotifyRange(const INotifyRange& other) = delete;
INotifyRange& operator=(const INotifyRange& other) = delete;

/// @brief The iterator giving InotifyEvents.
/// @warning The iterator's lifetime must not exceed the `INotifyRange` it
/// was obtained from.
class iterator
{
public:
using iterator_category = std::input_iterator_tag;
using value_type = score::os::InotifyEvent;
using difference_type = std::ptrdiff_t;
using pointer = const score::os::InotifyEvent*;
using reference = const score::os::InotifyEvent&;

explicit iterator(score::os::InotifyInstance* instance = nullptr) noexcept : instance_(instance)
{
if ((instance_ != nullptr) && !advance())
{
instance_ = nullptr;
}
}

[[nodiscard]] reference operator*() const
{
return events_[event_index_];
}

[[nodiscard]] pointer operator->() const
{
return &events_[event_index_];
}

iterator& operator++()
{
// we are end iterator
if (instance_ == nullptr)
{
return *this;
}

// become end iterator
if (!advance())
{
instance_ = nullptr;
}

return *this;
}

iterator operator++(int)
{
auto tmp = *this;
++(*this);
return tmp;
}

[[nodiscard]] bool operator==(const iterator& other) const
{
return instance_ == other.instance_;
}

[[nodiscard]] bool operator!=(const iterator& other) const
{
return !(*this == other);
}

private:
[[nodiscard]] bool advance()
{

SCORE_LANGUAGE_FUTURECPP_ASSERT_MESSAGE(
instance_ != nullptr, "The parent INotifyRange must have been deleted, documented as UB!");

const bool events_in_internal_buffer = event_index_ + 1U < events_.size();
if (events_in_internal_buffer)
{
++event_index_;
return true;
}

auto result = instance_->Read();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understood, this Read() call blocks until there is an event.
How to interrupt this when the ready timeout is over?
Or the startup needs to be aborted?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InotifyInstance.Close() aborts this

if (!result.has_value())
{
return false;
}

events_ = std::move(result.value());

if (events_.empty())
{
return false;
}

event_index_ = 0U;
return true;
}

/// @brief The underlying `InotifyInstance` instance.
score::os::InotifyInstance* instance_{nullptr};

/// @brief Internal buffer of events.
score::cpp::static_vector<score::os::InotifyEvent, score::os::InotifyInstance::max_events> events_{};
Comment thread
MaciejKaszynski marked this conversation as resolved.

/// @brief Index to the next event to give from the internal buffer.
std::size_t event_index_{0U};
};

[[nodiscard]] iterator begin() noexcept
{
return iterator{instance_.get()};
}

[[nodiscard]] iterator end() noexcept
{
return iterator{nullptr};
}

/// @brief Abort the inotify instance by closing it.
void stop() noexcept
{
if (instance_)
{
instance_->Close();
}
}

private:
std::unique_ptr<score::os::InotifyInstance> instance_;
};

} // namespace score::mw::lifecycle

#endif // SCORE_MW_LIFECYCLE_OSAL_INOTIFY_RANGE_HPP_
139 changes: 139 additions & 0 deletions score/launch_manager/src/daemon/src/osal/inotify_range_UT.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
#include "score/mw/launch_manager/osal/inotify_range.hpp"
#include "score/os/utils/inotify/inotify_instance_mock.h"
#include <gtest/gtest.h>
#include <memory>

namespace score::mw::lifecycle::testing
{

using score::os::Error;
using score::os::MakeFakeEvent;
using ::testing::Return;

using ReadRetT =
score::cpp::expected<score::cpp::static_vector<score::os::InotifyEvent, score::os::InotifyInstance::max_events>,
Error>;

using namespace std::string_view_literals;

class INotifyRangeUT : public ::testing::Test
{
protected:
void SetUp() override
{
RecordProperty("TestType", "interface-test");
RecordProperty("DerivationTechnique", "equivalence-classes");
}
};

TEST_F(INotifyRangeUT, NormalUsage)
{
/// Given we Read() one event
ReadRetT out{};
out->push_back(MakeFakeEvent(1,1,1, "hello"));
ReadRetT empty{};
auto mock_ptr = std::make_unique<score::os::InotifyInstanceMock>();
EXPECT_CALL(*mock_ptr, Read()).WillOnce(Return(out)).WillOnce(Return(empty));

INotifyRange range{std::move(mock_ptr)};

/// Expect the first value will be given.
auto iterator = range.begin();
auto event = *iterator;
EXPECT_EQ(event.GetCookie(), 1);

/// Expect incrementing will give you an end iterator.
iterator++;
EXPECT_EQ(iterator, range.end());
}

TEST_F(INotifyRangeUT, AdvanceWithinBufferedEvents)
{
/// Given we Read() 2 events, then 0 events
ReadRetT out{};
out->push_back(MakeFakeEvent(1, 1, 10, "first"));
out->push_back(MakeFakeEvent(2, 1, 20, "second"));
ReadRetT empty{};
auto mock_ptr = std::make_unique<score::os::InotifyInstanceMock>();
EXPECT_CALL(*mock_ptr, Read()).WillOnce(Return(out)).WillOnce(Return(empty));

INotifyRange range{std::move(mock_ptr)};

/// Expect that the first value will equal to the first event.
auto iterator = range.begin();
EXPECT_EQ(iterator->GetCookie(), 10);

/// Expect that the second value will equal to the second event.
++iterator;
EXPECT_EQ(iterator->GetCookie(), 20);

/// Expect that incrementing will give you an end iterator.
++iterator;
EXPECT_EQ(iterator, range.end());
}

TEST_F(INotifyRangeUT, AdvanceEndIterator)
{
auto mock_ptr = std::make_unique<score::os::InotifyInstanceMock>();
INotifyRange range{std::move(mock_ptr)};

/// Expect that incrementing end iterator still equals to an end iterator.
auto iterator = range.end();
iterator++;
EXPECT_EQ(iterator, range.end());
}

TEST_F(INotifyRangeUT, AdvanceError)
{
/// Given we get an Error from the Read().
auto mock_ptr = std::make_unique<score::os::InotifyInstanceMock>();
EXPECT_CALL(*mock_ptr, Read()).WillOnce(Return(score::cpp::unexpected(score::os::Error::createFromErrno(EINVAL))));

INotifyRange range{std::move(mock_ptr)};

/// Expect that the given iterator is empty.
EXPECT_EQ(range.begin(), range.end());
}

TEST_F(INotifyRangeUT, AbortCallsClose)
{
/// Given we have a mock InotifyInstance
auto mock_ptr = std::make_unique<score::os::InotifyInstanceMock>();
auto* mock_raw_ptr = mock_ptr.get();

/// Expect that Close() will be called on abort
EXPECT_CALL(*mock_raw_ptr, Close()).Times(1);

INotifyRange range{std::move(mock_ptr)};

/// When we call abort
range.stop();

/// Then Close() should have been called (verified by the expectation)
}

TEST_F(INotifyRangeUT, AbortWithNullInstance)
{
/// Given we have a range with no instance (moved out)
auto mock_ptr = std::make_unique<score::os::InotifyInstanceMock>();
INotifyRange range{std::move(mock_ptr)};
INotifyRange moved_range{std::move(range)};

/// When we call abort on the moved-from range
/// Then it should not crash (no Close() call expected since instance is null)
range.stop();
}

} // namespace score::mw::lifecycle::testing
Loading