| // Copyright 2020 The Chromium OS 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 "croslog/file_change_watcher.h" |
| |
| #include <map> |
| #include <utility> |
| #include <vector> |
| |
| #include <sys/inotify.h> |
| #include <sys/ioctl.h> |
| |
| #include "base/bind.h" |
| #include "base/files/scoped_file.h" |
| #include "base/logging.h" |
| #include "base/no_destructor.h" |
| #include "base/posix/eintr_wrapper.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/trace_event/trace_event.h" |
| |
| namespace croslog { |
| |
| namespace { |
| |
| // ============================================================================ |
| // InotifyReaderThread |
| |
| class InotifyReaderThread : public base::PlatformThread::Delegate { |
| public: |
| class Delegate { |
| public: |
| virtual void OnChanged(int inotify_wd, uint32_t mask) = 0; |
| }; |
| |
| // Must be called on the main thread. |
| InotifyReaderThread(scoped_refptr<base::SequencedTaskRunner> task_runner, |
| Delegate* delegate) |
| : task_runner_(std::move(task_runner)), delegate_(delegate) { |
| DCHECK(delegate_); |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| } |
| |
| // Must be called on the main thread. |
| void StartThread(int inotify_fd) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| inotify_fd_ = inotify_fd; |
| |
| CHECK(base::PlatformThread::CreateNonJoinable(0, this)); |
| } |
| |
| private: |
| scoped_refptr<base::SequencedTaskRunner> task_runner_; |
| int inotify_fd_ = -1; |
| Delegate* const delegate_; |
| |
| // Must be called on the worker thread. |
| void ThreadMain() { |
| DCHECK(!task_runner_->RunsTasksInCurrentSequence()); |
| base::PlatformThread::SetName("inotify_reader"); |
| |
| RunLoop(); |
| |
| // The code after RunLoop() won't be executed except for error cases. |
| // TODO(yoshiki): Shutdown this thread gracefully, |
| LOG(ERROR) << "Failed to wait for file change events."; |
| } |
| |
| // Must be called on the worker thread. |
| void RunLoop() { |
| DCHECK(!task_runner_->RunsTasksInCurrentSequence()); |
| |
| // Make sure the file descriptors are good for use with select(). |
| // TODO(yoshiki): Use epoll(2) or base::FileDescriptorWatcher instead. |
| CHECK_LE(0, inotify_fd_); |
| CHECK_GT(FD_SETSIZE, inotify_fd_); |
| |
| while (true) { |
| fd_set rfds; |
| FD_ZERO(&rfds); |
| FD_SET(inotify_fd_, &rfds); |
| |
| // Wait until some inotify events are available. |
| int select_result = HANDLE_EINTR( |
| select(inotify_fd_ + 1, &rfds, nullptr, nullptr, nullptr)); |
| if (select_result < 0) { |
| DPLOG(WARNING) << "select failed"; |
| return; |
| } |
| |
| // Adjust buffer size to current event queue size. |
| int buffer_size; |
| int ioctl_result = |
| HANDLE_EINTR(ioctl(inotify_fd_, FIONREAD, &buffer_size)); |
| |
| if (ioctl_result != 0) { |
| DPLOG(WARNING) << "ioctl failed"; |
| return; |
| } |
| |
| std::vector<char> buffer(buffer_size); |
| |
| ssize_t bytes_read = |
| HANDLE_EINTR(read(inotify_fd_, &buffer[0], buffer_size)); |
| |
| if (bytes_read < 0) { |
| DPLOG(WARNING) << "read from inotify fd failed"; |
| return; |
| } |
| |
| ssize_t i = 0; |
| while (i < bytes_read) { |
| inotify_event* event = reinterpret_cast<inotify_event*>(&buffer[i]); |
| size_t event_size = sizeof(inotify_event) + event->len; |
| DCHECK(i + event_size <= static_cast<size_t>(bytes_read)); |
| |
| PostInotifyEvent(event); |
| |
| i += event_size; |
| } |
| } |
| } |
| |
| // Must be called on the worker thread. |
| void PostInotifyEvent(inotify_event* event) { |
| DCHECK(!task_runner_->RunsTasksInCurrentSequence()); |
| |
| // This method is invoked on the Inotify thread. Switch to task_runner() to |
| // access |watches_| safely. Use a WeakPtr to prevent the callback from |
| // running after |this| is destroyed (i.e. after the watch is cancelled). |
| task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&Delegate::OnChanged, base::Unretained(delegate_), |
| event->wd, event->mask)); |
| } |
| }; |
| |
| // ============================================================================ |
| // FileChangeWatcherImpl |
| |
| class FileChangeWatcherImpl : public FileChangeWatcher, |
| public InotifyReaderThread::Delegate { |
| public: |
| FileChangeWatcherImpl() |
| : task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| thread_(task_runner_, this) { |
| // TODO(yoshiki): Handle log rotate. |
| |
| inotify_fd_.reset(inotify_init()); |
| PCHECK(inotify_fd_.is_valid()) << "inotify_init() failed"; |
| |
| thread_.StartThread(inotify_fd_.get()); |
| } |
| FileChangeWatcherImpl(const FileChangeWatcherImpl&) = delete; |
| FileChangeWatcherImpl& operator=(const FileChangeWatcherImpl&) = delete; |
| |
| // Note: This class is initialized with base::NoDestructor so its destructor |
| // is never called. |
| |
| bool AddWatch(const base::FilePath& path, |
| FileChangeWatcher::Observer* observer) override { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(observer != nullptr); |
| |
| int inotify_wd = inotify_add_watch(inotify_fd_.get(), path.value().c_str(), |
| IN_MODIFY | IN_MOVE_SELF); |
| |
| if (inotify_wd == -1) { |
| DPLOG(ERROR) << "inotify_add_watch (" << path << ") failed"; |
| return false; |
| } |
| |
| CHECK(watchers_inotify_.find(path) == watchers_inotify_.end()); |
| CHECK(watchers_observer_.find(inotify_wd) == watchers_observer_.end()); |
| |
| watchers_inotify_[path] = inotify_wd; |
| watchers_observer_[inotify_wd] = observer; |
| |
| return true; |
| } |
| |
| void RemoveWatch(const base::FilePath& path) override { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| if (watchers_inotify_.find(path) == watchers_inotify_.end()) { |
| LOG(WARNING) << "Unable to remove path: " << path << " is not added."; |
| return; |
| } |
| |
| int inotify_wd = watchers_inotify_[path]; |
| |
| CHECK(watchers_observer_.find(inotify_wd) != watchers_observer_.end()); |
| watchers_observer_.erase(inotify_wd); |
| watchers_inotify_.erase(path); |
| |
| auto inotify_wd_it = |
| std::find(unexpectedly_removed_inotify_wds_.begin(), |
| unexpectedly_removed_inotify_wds_.end(), inotify_wd); |
| bool already_removed_unexpectedly = |
| inotify_wd_it != unexpectedly_removed_inotify_wds_.end(); |
| |
| int ret = inotify_rm_watch(inotify_fd_.get(), inotify_wd); |
| if (ret == -1 && !already_removed_unexpectedly) |
| DPLOG(WARNING) << "inotify_rm_watch (" << path << ") failed"; |
| |
| if (already_removed_unexpectedly) |
| unexpectedly_removed_inotify_wds_.erase(inotify_wd_it); |
| } |
| |
| private: |
| base::ScopedFD inotify_fd_; |
| scoped_refptr<base::SequencedTaskRunner> task_runner_; |
| InotifyReaderThread thread_; |
| |
| std::map<base::FilePath, int> watchers_inotify_; |
| std::map<int, FileChangeWatcher::Observer*> watchers_observer_; |
| std::vector<int> unexpectedly_removed_inotify_wds_; |
| |
| void OnChanged(int inotify_wd, uint32_t mask) override { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| // |inotify_wd| is negative if the event queue is overflowed. |
| if (inotify_wd == -1) { |
| for (auto&& i : watchers_observer_) { |
| auto&& callback = i.second; |
| callback->OnFileContentMaybeChanged(); |
| callback->OnFileNameMaybeChanged(); |
| } |
| return; |
| } |
| if (watchers_observer_.find(inotify_wd) == watchers_observer_.end()) { |
| // Timing issue. Maybe the inotify observer was removed but some |
| // remaining event have been queued. Ignore them. |
| return; |
| } |
| |
| if (mask & IN_MODIFY) { |
| auto&& delegate = watchers_observer_[inotify_wd]; |
| delegate->OnFileContentMaybeChanged(); |
| } |
| |
| if (mask & IN_MOVE_SELF) { |
| auto&& delegate = watchers_observer_[inotify_wd]; |
| delegate->OnFileNameMaybeChanged(); |
| |
| // Don't remove and add the inotify here. The user will do that, since |
| // new file is unlikely to be created yet at this point. |
| } |
| |
| if (mask & IN_IGNORED) { |
| if (watchers_observer_.find(inotify_wd) != watchers_observer_.end()) { |
| LOG(WARNING) << "The inofity has been removed unexpectedly (maybe the " |
| "file was removed?)."; |
| unexpectedly_removed_inotify_wds_.push_back(inotify_wd); |
| } |
| } |
| } |
| }; |
| |
| } // anonymous namespace |
| |
| // ============================================================================ |
| // FileChangeWatcher |
| |
| // static |
| FileChangeWatcher* FileChangeWatcher::GetInstance() { |
| static base::NoDestructor<FileChangeWatcherImpl> change_watcher; |
| return change_watcher.get(); |
| } |
| |
| FileChangeWatcher::FileChangeWatcher() = default; |
| |
| } // namespace croslog |