Serenity Operating System
1/*
2 * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "FileWatcher.h"
8#include <AK/Debug.h>
9#include <AK/DeprecatedString.h>
10#include <AK/LexicalPath.h>
11#include <LibCore/Notifier.h>
12#include <errno.h>
13#include <limits.h>
14#include <string.h>
15#include <sys/inotify.h>
16#include <sys/ioctl.h>
17#include <unistd.h>
18
19#if !defined(AK_OS_LINUX)
20static_assert(false, "This file must only be used for Linux");
21#endif
22
23namespace Core {
24
25static constexpr unsigned file_watcher_flags_to_inotify_flags(FileWatcherFlags flags)
26{
27 unsigned result = 0;
28
29 if (has_flag(flags, FileWatcherFlags::Nonblock))
30 result |= IN_NONBLOCK;
31 if (has_flag(flags, FileWatcherFlags::CloseOnExec))
32 result |= IN_CLOEXEC;
33
34 return result;
35}
36
37static Optional<FileWatcherEvent> get_event_from_fd(int fd, HashMap<unsigned, DeprecatedString> const& wd_to_path)
38{
39 static constexpr auto max_event_size = sizeof(inotify_event) + NAME_MAX + 1;
40
41 // Note from INOTIFY(7) man page:
42 //
43 // Some systems cannot read integer variables if they are not properly aligned. On other
44 // systems, incorrect alignment may decrease performance. Hence, the buffer used for reading
45 // from the inotify file descriptor should have the same alignment as inotify_event.
46 alignas(alignof(inotify_event)) Array<u8, max_event_size> buffer;
47 ssize_t rc = ::read(fd, buffer.data(), buffer.size());
48
49 if (rc == 0) {
50 return {};
51 } else if (rc < 0) {
52 dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Reading from wd {} failed: {}", fd, strerror(errno));
53 return {};
54 }
55
56 auto const* event = reinterpret_cast<inotify_event const*>(buffer.data());
57 FileWatcherEvent result;
58
59 auto it = wd_to_path.find(event->wd);
60 if (it == wd_to_path.end()) {
61 dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Got an event for a non-existent wd {}?!", event->wd);
62 return {};
63 }
64
65 auto const& path = it->value;
66
67 if ((event->mask & IN_CREATE) != 0)
68 result.type |= FileWatcherEvent::Type::ChildCreated;
69 if ((event->mask & IN_DELETE) != 0)
70 result.type |= FileWatcherEvent::Type::ChildDeleted;
71 if ((event->mask & IN_DELETE_SELF) != 0)
72 result.type |= FileWatcherEvent::Type::Deleted;
73 if ((event->mask & IN_MODIFY) != 0)
74 result.type |= FileWatcherEvent::Type::ContentModified;
75 if ((event->mask & IN_ATTRIB) != 0)
76 result.type |= FileWatcherEvent::Type::MetadataModified;
77
78 if (result.type == FileWatcherEvent::Type::Invalid) {
79 warnln("Unknown event type {:x} returned by the watch_file descriptor for {}", event->mask, path);
80 return {};
81 }
82
83 if (event->len > 0) {
84 StringView child_name { event->name, strlen(event->name) };
85 result.event_path = LexicalPath::join(path, child_name).string();
86 } else {
87 result.event_path = path;
88 }
89
90 dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: got event from wd {} on '{}' type {}", fd, result.event_path, result.type);
91 return result;
92}
93
94ErrorOr<NonnullRefPtr<FileWatcher>> FileWatcher::create(FileWatcherFlags flags)
95{
96 auto watcher_fd = ::inotify_init1(file_watcher_flags_to_inotify_flags(flags | FileWatcherFlags::CloseOnExec));
97 if (watcher_fd < 0)
98 return Error::from_errno(errno);
99
100 auto notifier = TRY(Notifier::try_create(watcher_fd, Notifier::Event::Read));
101 return adopt_nonnull_ref_or_enomem(new (nothrow) FileWatcher(watcher_fd, move(notifier)));
102}
103
104FileWatcher::FileWatcher(int watcher_fd, NonnullRefPtr<Notifier> notifier)
105 : FileWatcherBase(watcher_fd)
106 , m_notifier(move(notifier))
107{
108 m_notifier->on_ready_to_read = [this] {
109 auto maybe_event = get_event_from_fd(m_notifier->fd(), m_wd_to_path);
110 if (maybe_event.has_value()) {
111 auto event = maybe_event.value();
112 on_change(event);
113
114 if (has_flag(event.type, FileWatcherEvent::Type::Deleted)) {
115 auto result = remove_watch(event.event_path);
116 if (result.is_error()) {
117 dbgln_if(FILE_WATCHER_DEBUG, "on_ready_to_read: {}", result.error());
118 }
119 }
120 }
121 };
122}
123
124FileWatcher::~FileWatcher() = default;
125
126ErrorOr<bool> FileWatcherBase::add_watch(DeprecatedString path, FileWatcherEvent::Type event_mask)
127{
128 if (m_path_to_wd.find(path) != m_path_to_wd.end()) {
129 dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", path);
130 return false;
131 }
132
133 unsigned inotify_mask = 0;
134
135 if (has_flag(event_mask, FileWatcherEvent::Type::ChildCreated))
136 inotify_mask |= IN_CREATE;
137 if (has_flag(event_mask, FileWatcherEvent::Type::ChildDeleted))
138 inotify_mask |= IN_DELETE;
139 if (has_flag(event_mask, FileWatcherEvent::Type::Deleted))
140 inotify_mask |= IN_DELETE_SELF;
141 if (has_flag(event_mask, FileWatcherEvent::Type::ContentModified))
142 inotify_mask |= IN_MODIFY;
143 if (has_flag(event_mask, FileWatcherEvent::Type::MetadataModified))
144 inotify_mask |= IN_ATTRIB;
145
146 int watch_descriptor = ::inotify_add_watch(m_watcher_fd, path.characters(), inotify_mask);
147 if (watch_descriptor < 0)
148 return Error::from_errno(errno);
149
150 m_path_to_wd.set(path, watch_descriptor);
151 m_wd_to_path.set(watch_descriptor, path);
152
153 dbgln_if(FILE_WATCHER_DEBUG, "add_watch: watching path '{}' on InodeWatcher {} wd {}", path, m_watcher_fd, watch_descriptor);
154 return true;
155}
156
157ErrorOr<bool> FileWatcherBase::remove_watch(DeprecatedString path)
158{
159 auto it = m_path_to_wd.find(path);
160 if (it == m_path_to_wd.end()) {
161 dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", path);
162 return false;
163 }
164
165 if (::inotify_rm_watch(m_watcher_fd, it->value) < 0)
166 return Error::from_errno(errno);
167
168 m_path_to_wd.remove(it);
169 m_wd_to_path.remove(it->value);
170
171 dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: stopped watching path '{}' on InodeWatcher {}", path, m_watcher_fd);
172 return true;
173}
174
175}