| // Copyright 2024 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| #include "pw_digital_io_linux/notifier.h" |
| |
| #include <sys/epoll.h> |
| #include <sys/eventfd.h> |
| #include <unistd.h> |
| |
| #include <array> |
| #include <cerrno> |
| #include <cstring> |
| #include <mutex> |
| #include <utility> |
| |
| #include "log_errno.h" |
| #include "pw_assert/check.h" |
| #include "pw_log/log.h" |
| #include "pw_status/status.h" |
| |
| namespace pw::digital_io { |
| |
| namespace { |
| |
| using pw::OkStatus; |
| |
| constexpr void* kCancelToken = nullptr; |
| |
| // The max number of that the notifier will process in a single iteration. |
| inline constexpr auto kMaxEventsPerWake = 16; |
| |
| } // namespace |
| |
| pw::Result<std::shared_ptr<LinuxGpioNotifier>> LinuxGpioNotifier::Create() { |
| // Create file descriptors |
| int raw_epoll_fd = epoll_create1(0); |
| if (raw_epoll_fd < 0) { |
| LOG_ERROR_WITH_ERRNO("Failed to initialize epoll descriptor:", errno); |
| return pw::Status::Internal(); |
| } |
| auto epoll_fd = OwnedFd(raw_epoll_fd); |
| |
| int raw_event_fd = eventfd(0, 0); |
| if (raw_event_fd < 0) { |
| LOG_ERROR_WITH_ERRNO("Failed to initialize event descriptor:", errno); |
| return pw::Status::Internal(); |
| } |
| auto event_fd = OwnedFd(raw_event_fd); |
| |
| // Attempt to register event_fd with epoll_fd |
| epoll_event event = { |
| .events = EPOLLIN, |
| .data = {.ptr = kCancelToken}, |
| }; |
| if (epoll_ctl(epoll_fd.fd(), EPOLL_CTL_ADD, event_fd.fd(), &event) != 0) { |
| // There is no reason this should ever fail, except for a bug! |
| LOG_ERROR_WITH_ERRNO("Failed to add cancel event to epoll descriptor:", |
| errno); |
| return pw::Status::Internal(); |
| } |
| |
| // Initialization succeeded - create the object. |
| return std::shared_ptr<LinuxGpioNotifier>( |
| new LinuxGpioNotifier(std::move(epoll_fd), std::move(event_fd))); |
| } |
| |
| LinuxGpioNotifier::~LinuxGpioNotifier() { |
| PW_CHECK_INT_EQ( // Crash OK: prevent use-after-free via registered lines. |
| registered_line_count_, |
| 0, |
| "Destroying notifier with registered lines"); |
| // fds closed automatically |
| } |
| |
| pw::Status LinuxGpioNotifier::RegisterLine( |
| int fd, LinuxGpioNotifier::Handler& handler) { |
| // Register for event notifications. Note that it's not clear from the |
| // documentation if EPOLLIN or EPOLLPRI is needed here, but EPOLLPRI shows up |
| // in all the examples online, and EPOLLIN is useful for testing. |
| epoll_event event = { |
| .events = EPOLLIN | EPOLLPRI, |
| .data = {.ptr = &handler}, |
| }; |
| if (epoll_ctl(epoll_fd_.fd(), EPOLL_CTL_ADD, fd, &event) != 0) { |
| switch (errno) { |
| case EBADF: |
| PW_LOG_WARN("The fd [%d] is invalid", fd); |
| return pw::Status::InvalidArgument(); |
| case EEXIST: |
| PW_LOG_WARN( |
| "The fd [%d] is already registered with epoll descriptor [%d]", |
| fd, |
| epoll_fd_.fd()); |
| return pw::Status::FailedPrecondition(); |
| case ENOSPC: |
| PW_LOG_WARN("No space to add fd [%d] to epoll descriptor [%d]", |
| fd, |
| epoll_fd_.fd()); |
| return pw::Status::ResourceExhausted(); |
| } |
| // Other errors are likely the result of bugs and should never happen. |
| LOG_ERROR_WITH_ERRNO("Failed to add fd [%d] to epoll descriptor [%d]:", |
| errno, |
| fd, |
| epoll_fd_.fd()); |
| return pw::Status::Internal(); |
| } |
| ++registered_line_count_; |
| return OkStatus(); |
| } |
| |
| pw::Status LinuxGpioNotifier::UnregisterLine(int fd) { |
| // Unregister from event notifications. |
| epoll_event unused{}; // See BUGS under epoll_ctl(2). |
| if (epoll_ctl(epoll_fd_.fd(), EPOLL_CTL_DEL, fd, &unused) != 0) { |
| switch (errno) { |
| case ENOENT: |
| // The file descriptor was not registered. |
| return pw::Status::NotFound(); |
| case EBADF: |
| PW_LOG_WARN("The fd [%d] is invalid", fd); |
| return pw::Status::InvalidArgument(); |
| } |
| // Other errors are likely the result of bugs and should never happen. |
| LOG_ERROR_WITH_ERRNO("Failed to remove fd [%d] from epoll descriptor [%d]:", |
| errno, |
| fd, |
| epoll_fd_.fd()); |
| return pw::Status::Internal(); |
| } |
| --registered_line_count_; |
| return OkStatus(); |
| } |
| |
| void LinuxGpioNotifier::CancelWait() { |
| uint64_t value = 1; |
| // Note this is used in tests only, and failure to cancel will hang the test |
| // or leak a thread - depending on if the test tries to join the thread. |
| ssize_t result = cancel_event_fd_.write(&value, sizeof(value)); |
| PW_DCHECK_INT_EQ(result, |
| sizeof(value), |
| "Failed to write cancel event: " ERRNO_FORMAT_STRING, |
| ERRNO_FORMAT_ARGS(errno)); |
| } |
| |
| pw::Result<unsigned int> LinuxGpioNotifier::WaitForEvents(int timeout_ms) { |
| // Block until there is at least 1 file descriptor with an event. |
| std::array<epoll_event, kMaxEventsPerWake> events{}; |
| int event_count; |
| for (;;) { |
| errno = 0; |
| event_count = |
| epoll_wait(epoll_fd_.fd(), events.data(), events.size(), timeout_ms); |
| if (event_count > 0) { |
| break; |
| } |
| if (event_count == 0) { |
| return pw::Status::DeadlineExceeded(); |
| } |
| if (errno == EINTR) { |
| // Call was interrupted by a signal to the thread. Restart it. |
| // NOTE: We don't attempt to update timeout_ms. |
| continue; |
| } |
| LOG_CRITICAL_WITH_ERRNO("Failed to wait on epoll descriptor:", errno); |
| return pw::Status::Internal(); |
| } |
| |
| // Process any lines that have events. Note that if event_count = |
| // kMaxEvents and there are more events waiting, we will get them on the |
| // next loop. |
| for (int i = 0; i < event_count; i++) { |
| if (events[i].data.ptr == kCancelToken) { |
| return pw::Status::Cancelled(); |
| } |
| static_cast<Handler*>(events[i].data.ptr)->HandleEvents(); |
| } |
| |
| // Must be positive due to (event_count > 0) check above. |
| return event_count; |
| } |
| |
| void LinuxGpioNotifier::Run() { |
| for (;;) { |
| auto status = WaitForEvents(-1); |
| if (!status.ok()) { |
| break; |
| } |
| } |
| } |
| |
| } // namespace pw::digital_io |