Serenity Operating System
1/*
2 * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
3 * Copyright (c) 2021, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "FileWatcher.h"
9#include <AK/Debug.h>
10#include <AK/DeprecatedString.h>
11#include <AK/LexicalPath.h>
12#include <AK/NonnullRefPtr.h>
13#include <Kernel/API/InodeWatcherEvent.h>
14#include <Kernel/API/InodeWatcherFlags.h>
15#include <LibCore/Notifier.h>
16#include <errno.h>
17#include <fcntl.h>
18#include <string.h>
19#include <unistd.h>
20
21#if !defined(AK_OS_SERENITY)
22static_assert(false, "This file must only be used for SerenityOS");
23#endif
24
25namespace Core {
26
27static constexpr unsigned file_watcher_flags_to_inode_watcher_flags(FileWatcherFlags flags)
28{
29 auto result = InodeWatcherFlags::None;
30
31 if (has_flag(flags, FileWatcherFlags::Nonblock))
32 result |= InodeWatcherFlags::Nonblock;
33 if (has_flag(flags, FileWatcherFlags::CloseOnExec))
34 result |= InodeWatcherFlags::CloseOnExec;
35
36 return static_cast<unsigned>(result);
37}
38
39static Optional<FileWatcherEvent> get_event_from_fd(int fd, HashMap<unsigned, DeprecatedString> const& wd_to_path)
40{
41 u8 buffer[MAXIMUM_EVENT_SIZE];
42 int rc = read(fd, &buffer, MAXIMUM_EVENT_SIZE);
43 if (rc == 0) {
44 return {};
45 } else if (rc < 0) {
46 dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Reading from wd {} failed: {}", fd, strerror(errno));
47 return {};
48 }
49
50 InodeWatcherEvent* event = reinterpret_cast<InodeWatcherEvent*>(buffer);
51 FileWatcherEvent result;
52
53 auto it = wd_to_path.find(event->watch_descriptor);
54 if (it == wd_to_path.end()) {
55 dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Got an event for a non-existent wd {}?!", event->watch_descriptor);
56 return {};
57 }
58 DeprecatedString const& path = it->value;
59
60 switch (event->type) {
61 case InodeWatcherEvent::Type::ChildCreated:
62 result.type = FileWatcherEvent::Type::ChildCreated;
63 break;
64 case InodeWatcherEvent::Type::ChildDeleted:
65 result.type = FileWatcherEvent::Type::ChildDeleted;
66 break;
67 case InodeWatcherEvent::Type::Deleted:
68 result.type = FileWatcherEvent::Type::Deleted;
69 break;
70 case InodeWatcherEvent::Type::ContentModified:
71 result.type = FileWatcherEvent::Type::ContentModified;
72 break;
73 case InodeWatcherEvent::Type::MetadataModified:
74 result.type = FileWatcherEvent::Type::MetadataModified;
75 break;
76 default:
77 warnln("Unknown event type {} returned by the watch_file descriptor for {}", static_cast<unsigned>(event->type), path);
78 return {};
79 }
80
81 // We trust that the kernel only sends the name when appropriate.
82 if (event->name_length > 0) {
83 DeprecatedString child_name { event->name, event->name_length - 1 };
84 result.event_path = LexicalPath::join(path, child_name).string();
85 } else {
86 result.event_path = path;
87 }
88
89 dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: got event from wd {} on '{}' type {}", fd, result.event_path, result.type);
90 return result;
91}
92
93static DeprecatedString canonicalize_path(DeprecatedString path)
94{
95 if (!path.is_empty() && path[0] == '/')
96 return LexicalPath::canonicalized_path(move(path));
97 char* cwd = getcwd(nullptr, 0);
98 VERIFY(cwd);
99 return LexicalPath::join({ cwd, strlen(cwd) }, move(path)).string();
100}
101
102ErrorOr<bool> FileWatcherBase::add_watch(DeprecatedString path, FileWatcherEvent::Type event_mask)
103{
104 DeprecatedString canonical_path = canonicalize_path(move(path));
105
106 if (m_path_to_wd.find(canonical_path) != m_path_to_wd.end()) {
107 dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", canonical_path);
108 return false;
109 }
110
111 auto kernel_mask = InodeWatcherEvent::Type::Invalid;
112 if (has_flag(event_mask, FileWatcherEvent::Type::ChildCreated))
113 kernel_mask |= InodeWatcherEvent::Type::ChildCreated;
114 if (has_flag(event_mask, FileWatcherEvent::Type::ChildDeleted))
115 kernel_mask |= InodeWatcherEvent::Type::ChildDeleted;
116 if (has_flag(event_mask, FileWatcherEvent::Type::Deleted))
117 kernel_mask |= InodeWatcherEvent::Type::Deleted;
118 if (has_flag(event_mask, FileWatcherEvent::Type::ContentModified))
119 kernel_mask |= InodeWatcherEvent::Type::ContentModified;
120 if (has_flag(event_mask, FileWatcherEvent::Type::MetadataModified))
121 kernel_mask |= InodeWatcherEvent::Type::MetadataModified;
122
123 int wd = inode_watcher_add_watch(m_watcher_fd, canonical_path.characters(), canonical_path.length(), static_cast<unsigned>(kernel_mask));
124 if (wd < 0)
125 return Error::from_errno(errno);
126
127 m_path_to_wd.set(canonical_path, wd);
128 m_wd_to_path.set(wd, canonical_path);
129
130 dbgln_if(FILE_WATCHER_DEBUG, "add_watch: watching path '{}' on InodeWatcher {} wd {}", canonical_path, m_watcher_fd, wd);
131 return true;
132}
133
134ErrorOr<bool> FileWatcherBase::remove_watch(DeprecatedString path)
135{
136 DeprecatedString canonical_path = canonicalize_path(move(path));
137
138 auto it = m_path_to_wd.find(canonical_path);
139 if (it == m_path_to_wd.end()) {
140 dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", canonical_path);
141 return false;
142 }
143
144 if (inode_watcher_remove_watch(m_watcher_fd, it->value) < 0)
145 return Error::from_errno(errno);
146
147 m_path_to_wd.remove(it);
148 m_wd_to_path.remove(it->value);
149
150 dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: stopped watching path '{}' on InodeWatcher {}", canonical_path, m_watcher_fd);
151 return true;
152}
153
154BlockingFileWatcher::BlockingFileWatcher(FileWatcherFlags flags)
155 : FileWatcherBase(create_inode_watcher(file_watcher_flags_to_inode_watcher_flags(flags)))
156{
157 VERIFY(m_watcher_fd != -1);
158 dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher created with InodeWatcher {}", m_watcher_fd);
159}
160
161BlockingFileWatcher::~BlockingFileWatcher()
162{
163 close(m_watcher_fd);
164}
165
166Optional<FileWatcherEvent> BlockingFileWatcher::wait_for_event()
167{
168 dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher::wait_for_event()");
169
170 auto maybe_event = get_event_from_fd(m_watcher_fd, m_wd_to_path);
171 if (!maybe_event.has_value())
172 return maybe_event;
173
174 auto event = maybe_event.release_value();
175 if (event.type == FileWatcherEvent::Type::Deleted) {
176 auto result = remove_watch(event.event_path);
177 if (result.is_error()) {
178 dbgln_if(FILE_WATCHER_DEBUG, "wait_for_event: {}", result.error());
179 }
180 }
181
182 return event;
183}
184
185ErrorOr<NonnullRefPtr<FileWatcher>> FileWatcher::create(FileWatcherFlags flags)
186{
187 auto watcher_fd = create_inode_watcher(file_watcher_flags_to_inode_watcher_flags(flags | FileWatcherFlags::CloseOnExec));
188 if (watcher_fd < 0)
189 return Error::from_errno(errno);
190
191 auto notifier = Notifier::construct(watcher_fd, Notifier::Event::Read);
192 return adopt_ref(*new FileWatcher(watcher_fd, move(notifier)));
193}
194
195FileWatcher::FileWatcher(int watcher_fd, NonnullRefPtr<Notifier> notifier)
196 : FileWatcherBase(watcher_fd)
197 , m_notifier(move(notifier))
198{
199 m_notifier->on_ready_to_read = [this] {
200 auto maybe_event = get_event_from_fd(m_notifier->fd(), m_wd_to_path);
201 if (maybe_event.has_value()) {
202 auto event = maybe_event.value();
203 on_change(event);
204
205 if (event.type == FileWatcherEvent::Type::Deleted) {
206 auto result = remove_watch(event.event_path);
207 if (result.is_error()) {
208 dbgln_if(FILE_WATCHER_DEBUG, "on_ready_to_read: {}", result.error());
209 }
210 }
211 }
212 };
213}
214
215FileWatcher::~FileWatcher()
216{
217 m_notifier->on_ready_to_read = nullptr;
218 close(m_notifier->fd());
219 dbgln_if(FILE_WATCHER_DEBUG, "Stopped watcher at fd {}", m_notifier->fd());
220}
221
222}