xref: /aosp_15_r20/external/cronet/base/files/file_path_watcher_win.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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