1 // Copyright 2011 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/files/file_path_watcher.h"
6
7 #include <windows.h>
8
9 #include "base/files/file.h"
10 #include "base/files/file_path.h"
11 #include "base/files/file_util.h"
12 #include "base/functional/bind.h"
13 #include "base/logging.h"
14 #include "base/memory/ptr_util.h"
15 #include "base/memory/raw_ptr.h"
16 #include "base/strings/string_util.h"
17 #include "base/task/sequenced_task_runner.h"
18 #include "base/threading/scoped_blocking_call.h"
19 #include "base/time/time.h"
20 #include "base/win/object_watcher.h"
21
22 namespace base {
23
24 namespace {
25
26 class FilePathWatcherImpl : public FilePathWatcher::PlatformDelegate,
27 public base::win::ObjectWatcher::Delegate {
28 public:
29 FilePathWatcherImpl() = default;
30 FilePathWatcherImpl(const FilePathWatcherImpl&) = delete;
31 FilePathWatcherImpl& operator=(const FilePathWatcherImpl&) = delete;
32 ~FilePathWatcherImpl() override;
33
34 // FilePathWatcher::PlatformDelegate:
35 bool Watch(const FilePath& path,
36 Type type,
37 const FilePathWatcher::Callback& callback) override;
38 void Cancel() override;
39
40 // base::win::ObjectWatcher::Delegate:
41 void OnObjectSignaled(HANDLE object) override;
42
43 private:
44 // Setup a watch handle for directory |dir|. Set |recursive| to true to watch
45 // the directory sub trees. Returns true if no fatal error occurs. |handle|
46 // will receive the handle value if |dir| is watchable, otherwise
47 // INVALID_HANDLE_VALUE.
48 [[nodiscard]] static bool SetupWatchHandle(const FilePath& dir,
49 bool recursive,
50 HANDLE* handle);
51
52 // (Re-)Initialize the watch handle.
53 [[nodiscard]] bool UpdateWatch();
54
55 // Destroy the watch handle.
56 void DestroyWatch();
57
58 // Callback to notify upon changes.
59 FilePathWatcher::Callback callback_;
60
61 // Path we're supposed to watch (passed to callback).
62 FilePath target_;
63
64 // Set to true in the destructor.
65 raw_ptr<bool> was_deleted_ptr_ = nullptr;
66
67 // Handle for FindFirstChangeNotification.
68 HANDLE handle_ = INVALID_HANDLE_VALUE;
69
70 // ObjectWatcher to watch handle_ for events.
71 base::win::ObjectWatcher watcher_;
72
73 // The type of watch requested.
74 Type type_ = Type::kNonRecursive;
75
76 // Keep track of the last modified time of the file. We use nulltime
77 // to represent the file not existing.
78 Time last_modified_;
79
80 // The time at which we processed the first notification with the
81 // |last_modified_| time stamp.
82 Time first_notification_;
83 };
84
~FilePathWatcherImpl()85 FilePathWatcherImpl::~FilePathWatcherImpl() {
86 DCHECK(!task_runner() || task_runner()->RunsTasksInCurrentSequence());
87 if (was_deleted_ptr_)
88 *was_deleted_ptr_ = true;
89 }
90
Watch(const FilePath & path,Type type,const FilePathWatcher::Callback & callback)91 bool FilePathWatcherImpl::Watch(const FilePath& path,
92 Type type,
93 const FilePathWatcher::Callback& callback) {
94 DCHECK(target_.value().empty()); // Can only watch one path.
95
96 set_task_runner(SequencedTaskRunner::GetCurrentDefault());
97 callback_ = callback;
98 target_ = path;
99 type_ = type;
100
101 File::Info file_info;
102 ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
103 if (GetFileInfo(target_, &file_info)) {
104 last_modified_ = file_info.last_modified;
105 first_notification_ = Time::Now();
106 }
107
108 if (!UpdateWatch())
109 return false;
110
111 watcher_.StartWatchingOnce(handle_, this);
112
113 return true;
114 }
115
Cancel()116 void FilePathWatcherImpl::Cancel() {
117 if (callback_.is_null()) {
118 // Watch was never called, or the |task_runner_| has already quit.
119 set_cancelled();
120 return;
121 }
122
123 DCHECK(task_runner()->RunsTasksInCurrentSequence());
124 set_cancelled();
125
126 if (handle_ != INVALID_HANDLE_VALUE)
127 DestroyWatch();
128
129 callback_.Reset();
130 }
131
OnObjectSignaled(HANDLE object)132 void FilePathWatcherImpl::OnObjectSignaled(HANDLE object) {
133 DCHECK(task_runner()->RunsTasksInCurrentSequence());
134 DCHECK_EQ(object, handle_);
135 DCHECK(!was_deleted_ptr_);
136
137 bool was_deleted = false;
138 was_deleted_ptr_ = &was_deleted;
139
140 if (!UpdateWatch()) {
141 callback_.Run(target_, true /* error */);
142 return;
143 }
144
145 // Check whether the event applies to |target_| and notify the callback.
146 File::Info file_info;
147 bool file_exists = false;
148 {
149 ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
150 file_exists = GetFileInfo(target_, &file_info);
151 }
152 if (type_ == Type::kRecursive) {
153 // Only the mtime of |target_| is tracked but in a recursive watch,
154 // some other file or directory may have changed so all notifications
155 // are passed through. It is possible to figure out which file changed
156 // using ReadDirectoryChangesW() instead of FindFirstChangeNotification(),
157 // but that function is quite complicated:
158 // http://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw.html
159 callback_.Run(target_, false);
160 } else if (file_exists && (last_modified_.is_null() ||
161 last_modified_ != file_info.last_modified)) {
162 last_modified_ = file_info.last_modified;
163 first_notification_ = Time::Now();
164 callback_.Run(target_, false);
165 } else if (file_exists && last_modified_ == file_info.last_modified &&
166 !first_notification_.is_null()) {
167 // The target's last modification time is equal to what's on record. This
168 // means that either an unrelated event occurred, or the target changed
169 // again (file modification times only have a resolution of 1s). Comparing
170 // file modification times against the wall clock is not reliable to find
171 // out whether the change is recent, since this code might just run too
172 // late. Moreover, there's no guarantee that file modification time and wall
173 // clock times come from the same source.
174 //
175 // Instead, the time at which the first notification carrying the current
176 // |last_notified_| time stamp is recorded. Later notifications that find
177 // the same file modification time only need to be forwarded until wall
178 // clock has advanced one second from the initial notification. After that
179 // interval, client code is guaranteed to having seen the current revision
180 // of the file.
181 if (Time::Now() - first_notification_ > Seconds(1)) {
182 // Stop further notifications for this |last_modification_| time stamp.
183 first_notification_ = Time();
184 }
185 callback_.Run(target_, false);
186 } else if (!file_exists && !last_modified_.is_null()) {
187 last_modified_ = Time();
188 callback_.Run(target_, false);
189 }
190
191 // The watch may have been cancelled by the callback.
192 if (!was_deleted) {
193 watcher_.StartWatchingOnce(handle_, this);
194 was_deleted_ptr_ = nullptr;
195 }
196 }
197
198 // static
SetupWatchHandle(const FilePath & dir,bool recursive,HANDLE * handle)199 bool FilePathWatcherImpl::SetupWatchHandle(const FilePath& dir,
200 bool recursive,
201 HANDLE* handle) {
202 ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
203 *handle = FindFirstChangeNotification(
204 dir.value().c_str(), recursive,
205 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE |
206 FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_DIR_NAME |
207 FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY);
208 if (*handle != INVALID_HANDLE_VALUE) {
209 // Make sure the handle we got points to an existing directory. It seems
210 // that windows sometimes hands out watches to directories that are
211 // about to go away, but doesn't sent notifications if that happens.
212 if (!DirectoryExists(dir)) {
213 FindCloseChangeNotification(*handle);
214 *handle = INVALID_HANDLE_VALUE;
215 }
216 return true;
217 }
218
219 // If FindFirstChangeNotification failed because the target directory
220 // doesn't exist, access is denied (happens if the file is already gone but
221 // there are still handles open), or the target is not a directory, try the
222 // immediate parent directory instead.
223 DWORD error_code = GetLastError();
224 if (error_code != ERROR_FILE_NOT_FOUND &&
225 error_code != ERROR_PATH_NOT_FOUND &&
226 error_code != ERROR_ACCESS_DENIED &&
227 error_code != ERROR_SHARING_VIOLATION &&
228 error_code != ERROR_DIRECTORY) {
229 DPLOG(ERROR) << "FindFirstChangeNotification failed for "
230 << dir.value();
231 return false;
232 }
233
234 return true;
235 }
236
UpdateWatch()237 bool FilePathWatcherImpl::UpdateWatch() {
238 if (handle_ != INVALID_HANDLE_VALUE)
239 DestroyWatch();
240
241 ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
242
243 // Start at the target and walk up the directory chain until we succesfully
244 // create a watch handle in |handle_|. |child_dirs| keeps a stack of child
245 // directories stripped from target, in reverse order.
246 std::vector<FilePath> child_dirs;
247 FilePath watched_path(target_);
248 while (true) {
249 if (!SetupWatchHandle(watched_path, type_ == Type::kRecursive, &handle_))
250 return false;
251
252 // Break if a valid handle is returned. Try the parent directory otherwise.
253 if (handle_ != INVALID_HANDLE_VALUE)
254 break;
255
256 // Abort if we hit the root directory.
257 child_dirs.push_back(watched_path.BaseName());
258 FilePath parent(watched_path.DirName());
259 if (parent == watched_path) {
260 DLOG(ERROR) << "Reached the root directory";
261 return false;
262 }
263 watched_path = parent;
264 }
265
266 // At this point, handle_ is valid. However, the bottom-up search that the
267 // above code performs races against directory creation. So try to walk back
268 // down and see whether any children appeared in the mean time.
269 while (!child_dirs.empty()) {
270 watched_path = watched_path.Append(child_dirs.back());
271 child_dirs.pop_back();
272 HANDLE temp_handle = INVALID_HANDLE_VALUE;
273 if (!SetupWatchHandle(watched_path, type_ == Type::kRecursive,
274 &temp_handle)) {
275 return false;
276 }
277 if (temp_handle == INVALID_HANDLE_VALUE)
278 break;
279 FindCloseChangeNotification(handle_);
280 handle_ = temp_handle;
281 }
282
283 return true;
284 }
285
DestroyWatch()286 void FilePathWatcherImpl::DestroyWatch() {
287 watcher_.StopWatching();
288
289 ScopedBlockingCall scoped_blocking_call(FROM_HERE, BlockingType::MAY_BLOCK);
290 FindCloseChangeNotification(handle_);
291 handle_ = INVALID_HANDLE_VALUE;
292 }
293
294 } // namespace
295
FilePathWatcher()296 FilePathWatcher::FilePathWatcher()
297 : FilePathWatcher(std::make_unique<FilePathWatcherImpl>()) {}
298
299 } // namespace base
300