1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /**
8  * Native implementation of Watcher operations.
9  */
10 #include "NativeFileWatcherWin.h"
11 
12 #include "mozilla/Services.h"
13 #include "mozilla/UniquePtr.h"
14 #include "nsClassHashtable.h"
15 #include "nsDataHashtable.h"
16 #include "nsIFile.h"
17 #include "nsIObserverService.h"
18 #include "nsProxyRelease.h"
19 #include "nsTArray.h"
20 #include "mozilla/Logging.h"
21 
22 namespace mozilla {
23 
24 // Enclose everything which is not exported in an anonymous namespace.
25 namespace {
26 
27 /**
28  * An event used to notify the main thread when an error happens.
29  */
30 class WatchedErrorEvent final : public Runnable {
31  public:
32   /**
33    * @param aOnError The passed error callback.
34    * @param aError The |nsresult| error value.
35    * @param osError The error returned by GetLastError().
36    */
WatchedErrorEvent(const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> & aOnError,const nsresult & anError,const DWORD & osError)37   WatchedErrorEvent(
38       const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
39       const nsresult& anError, const DWORD& osError)
40       : Runnable("WatchedErrorEvent"), mOnError(aOnError), mError(anError) {
41     MOZ_ASSERT(!NS_IsMainThread());
42   }
43 
Run()44   NS_IMETHOD Run() override {
45     MOZ_ASSERT(NS_IsMainThread());
46 
47     // Make sure we wrap a valid callback since it's not mandatory to provide
48     // one when watching a resource.
49     if (mOnError) {
50       (void)mOnError->Complete(mError, mOsError);
51     }
52 
53     return NS_OK;
54   }
55 
56  private:
57   nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> mOnError;
58   nsresult mError;
59   DWORD mOsError;
60 };
61 
62 /**
63  * An event used to notify the main thread when an operation is successful.
64  */
65 class WatchedSuccessEvent final : public Runnable {
66  public:
67   /**
68    * @param aOnSuccess The passed success callback.
69    * @param aResourcePath
70    *        The path of the resource for which this event was generated.
71    */
WatchedSuccessEvent(const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> & aOnSuccess,const nsAString & aResourcePath)72   WatchedSuccessEvent(const nsMainThreadPtrHandle<
73                           nsINativeFileWatcherSuccessCallback>& aOnSuccess,
74                       const nsAString& aResourcePath)
75       : Runnable("WatchedSuccessEvent"),
76         mOnSuccess(aOnSuccess),
77         mResourcePath(aResourcePath) {
78     MOZ_ASSERT(!NS_IsMainThread());
79   }
80 
Run()81   NS_IMETHOD Run() override {
82     MOZ_ASSERT(NS_IsMainThread());
83 
84     // Make sure we wrap a valid callback since it's not mandatory to provide
85     // one when watching a resource.
86     if (mOnSuccess) {
87       (void)mOnSuccess->Complete(mResourcePath);
88     }
89 
90     return NS_OK;
91   }
92 
93  private:
94   nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> mOnSuccess;
95   nsString mResourcePath;
96 };
97 
98 /**
99  * An event used to notify the main thread of a change in a watched
100  * resource.
101  */
102 class WatchedChangeEvent final : public Runnable {
103  public:
104   /**
105    * @param aOnChange The passed change callback.
106    * @param aChangedResource The name of the changed resource.
107    */
WatchedChangeEvent(const nsMainThreadPtrHandle<nsINativeFileWatcherCallback> & aOnChange,const nsAString & aChangedResource)108   WatchedChangeEvent(
109       const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
110       const nsAString& aChangedResource)
111       : Runnable("WatchedChangeEvent"),
112         mOnChange(aOnChange),
113         mChangedResource(aChangedResource) {
114     MOZ_ASSERT(!NS_IsMainThread());
115   }
116 
Run()117   NS_IMETHOD Run() override {
118     MOZ_ASSERT(NS_IsMainThread());
119 
120     // The second parameter is reserved for future uses: we use 0 as a
121     // placeholder.
122     (void)mOnChange->Changed(mChangedResource, 0);
123     return NS_OK;
124   }
125 
126  private:
127   nsMainThreadPtrHandle<nsINativeFileWatcherCallback> mOnChange;
128   nsString mChangedResource;
129 };
130 
131 static mozilla::LazyLogModule gNativeWatcherPRLog("NativeFileWatcherService");
132 #define FILEWATCHERLOG(...) \
133   MOZ_LOG(gNativeWatcherPRLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
134 
135 // The number of notifications to store within
136 // WatchedResourceDescriptor:mNotificationBuffer. If the buffer overflows, its
137 // contents are discarded and a change callback is dispatched with "*" as
138 // changed path.
139 const unsigned int WATCHED_RES_MAXIMUM_NOTIFICATIONS = 100;
140 
141 // The size, in bytes, of the notification buffer used to store the changes
142 // notifications for each watched resource.
143 const size_t NOTIFICATION_BUFFER_SIZE =
144     WATCHED_RES_MAXIMUM_NOTIFICATIONS * sizeof(FILE_NOTIFY_INFORMATION);
145 
146 /**
147  * AutoCloseHandle is a RAII wrapper for Windows |HANDLE|s
148  */
149 struct AutoCloseHandleTraits {
150   typedef HANDLE type;
emptymozilla::__anona00e5e5f0111::AutoCloseHandleTraits151   static type empty() { return INVALID_HANDLE_VALUE; }
releasemozilla::__anona00e5e5f0111::AutoCloseHandleTraits152   static void release(type anHandle) {
153     if (anHandle != INVALID_HANDLE_VALUE) {
154       // If CancelIo is called on an |HANDLE| not yet associated to a Completion
155       // I/O it simply does nothing.
156       (void)CancelIo(anHandle);
157       (void)CloseHandle(anHandle);
158     }
159   }
160 };
161 typedef Scoped<AutoCloseHandleTraits> AutoCloseHandle;
162 
163 // Define these callback array types to make the code easier to read.
164 typedef nsTArray<nsMainThreadPtrHandle<nsINativeFileWatcherCallback>>
165     ChangeCallbackArray;
166 typedef nsTArray<nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>>
167     ErrorCallbackArray;
168 
169 /**
170  * A structure to keep track of the information related to a
171  * watched resource.
172  */
173 struct WatchedResourceDescriptor {
174   // The path on the file system of the watched resource.
175   nsString mPath;
176 
177   // A buffer containing the latest notifications received for the resource.
178   // UniquePtr<FILE_NOTIFY_INFORMATION> cannot be used as the structure
179   // contains a variable length field (FileName).
180   UniquePtr<unsigned char> mNotificationBuffer;
181 
182   // Used to hold information for the asynchronous ReadDirectoryChangesW call
183   // (does not need to be closed as it is not an |HANDLE|).
184   OVERLAPPED mOverlappedInfo;
185 
186   // The OS handle to the watched resource.
187   AutoCloseHandle mResourceHandle;
188 
WatchedResourceDescriptormozilla::__anona00e5e5f0111::WatchedResourceDescriptor189   WatchedResourceDescriptor(const nsAString& aPath, const HANDLE anHandle)
190       : mPath(aPath), mResourceHandle(anHandle) {
191     memset(&mOverlappedInfo, 0, sizeof(OVERLAPPED));
192     mNotificationBuffer.reset(new unsigned char[NOTIFICATION_BUFFER_SIZE]);
193   }
194 };
195 
196 /**
197  * A structure used to pass the callbacks to the AddPathRunnableMethod() and
198  * RemovePathRunnableMethod().
199  */
200 struct PathRunnablesParametersWrapper {
201   nsString mPath;
202   nsMainThreadPtrHandle<nsINativeFileWatcherCallback> mChangeCallbackHandle;
203   nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> mErrorCallbackHandle;
204   nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>
205       mSuccessCallbackHandle;
206 
PathRunnablesParametersWrappermozilla::__anona00e5e5f0111::PathRunnablesParametersWrapper207   PathRunnablesParametersWrapper(
208       const nsAString& aPath,
209       const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
210       const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
211       const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>&
212           aOnSuccess)
213       : mPath(aPath),
214         mChangeCallbackHandle(aOnChange),
215         mErrorCallbackHandle(aOnError),
216         mSuccessCallbackHandle(aOnSuccess) {}
217 };
218 
219 /**
220  * This runnable is dispatched to the main thread in order to safely
221  * shutdown the worker thread.
222  */
223 class NativeWatcherIOShutdownTask : public Runnable {
224  public:
NativeWatcherIOShutdownTask()225   NativeWatcherIOShutdownTask()
226       : Runnable("NativeWatcherIOShutdownTask"),
227         mWorkerThread(do_GetCurrentThread()) {
228     MOZ_ASSERT(!NS_IsMainThread());
229   }
230 
Run()231   NS_IMETHOD Run() override {
232     MOZ_ASSERT(NS_IsMainThread());
233     mWorkerThread->Shutdown();
234     return NS_OK;
235   }
236 
237  private:
238   nsCOMPtr<nsIThread> mWorkerThread;
239 };
240 
241 /**
242  * This runnable is dispatched from the main thread to get the notifications of
243  * the changes in the watched resources by continuously calling the blocking
244  * function GetQueuedCompletionStatus. This function queries the status of the
245  * Completion I/O port initialized in the main thread. The watched resources are
246  * registered to the completion I/O port when calling |addPath|.
247  *
248  * Instead of using a loop within the Run() method, the Runnable reschedules
249  * itself by issuing a NS_DispatchToCurrentThread(this) before exiting. This is
250  * done to allow the execution of other runnables enqueued within the thread
251  * task queue.
252  */
253 class NativeFileWatcherIOTask : public Runnable {
254  public:
NativeFileWatcherIOTask(HANDLE aIOCompletionPort)255   explicit NativeFileWatcherIOTask(HANDLE aIOCompletionPort)
256       : Runnable("NativeFileWatcherIOTask"),
257         mIOCompletionPort(aIOCompletionPort),
258         mShuttingDown(false) {}
259 
260   NS_IMETHOD Run() override;
261   nsresult AddPathRunnableMethod(
262       PathRunnablesParametersWrapper* aWrappedParameters);
263   nsresult RemovePathRunnableMethod(
264       PathRunnablesParametersWrapper* aWrappedParameters);
265   nsresult DeactivateRunnableMethod();
266 
267  private:
268   // Maintain 2 indexes - one by resource path, one by resource |HANDLE|.
269   // Since |HANDLE| is basically a typedef to void*, we use nsVoidPtrHashKey to
270   // compute the hashing key. We need 2 indexes in order to quickly look up the
271   // changed resource in the Worker Thread.
272   // The objects are not ref counted and get destroyed by
273   // mWatchedResourcesByPath on NativeFileWatcherService::Destroy or in
274   // NativeFileWatcherService::RemovePath.
275   nsClassHashtable<nsStringHashKey, WatchedResourceDescriptor>
276       mWatchedResourcesByPath;
277   nsDataHashtable<nsVoidPtrHashKey, WatchedResourceDescriptor*>
278       mWatchedResourcesByHandle;
279 
280   // The same callback can be associated to multiple watches so we need to keep
281   // them alive as long as there is a watch using them. We create two hashtables
282   // to map directory names to lists of nsMainThreadPtr<callbacks>.
283   nsClassHashtable<nsStringHashKey, ChangeCallbackArray> mChangeCallbacksTable;
284   nsClassHashtable<nsStringHashKey, ErrorCallbackArray> mErrorCallbacksTable;
285 
286   // We hold a copy of the completion port |HANDLE|, which is owned by the main
287   // thread.
288   HANDLE mIOCompletionPort;
289 
290   // Other methods need to know that a shutdown is in progress.
291   bool mShuttingDown;
292 
293   nsresult RunInternal();
294 
295   nsresult DispatchChangeCallbacks(
296       WatchedResourceDescriptor* aResourceDescriptor,
297       const nsAString& aChangedResource);
298 
299   nsresult ReportChange(
300       const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
301       const nsAString& aChangedResource);
302 
303   nsresult DispatchErrorCallbacks(
304       WatchedResourceDescriptor* aResourceDescriptor, nsresult anError,
305       DWORD anOSError);
306 
307   nsresult ReportError(
308       const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
309       nsresult anError, DWORD anOSError);
310 
311   nsresult ReportSuccess(const nsMainThreadPtrHandle<
312                              nsINativeFileWatcherSuccessCallback>& aOnSuccess,
313                          const nsAString& aResourcePath);
314 
315   nsresult AddDirectoryToWatchList(
316       WatchedResourceDescriptor* aDirectoryDescriptor);
317 
318   void AppendCallbacksToHashtables(
319       const nsAString& aPath,
320       const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
321       const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError);
322 
323   void RemoveCallbacksFromHashtables(
324       const nsAString& aPath,
325       const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
326       const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError);
327 
328   nsresult MakeResourcePath(WatchedResourceDescriptor* changedDescriptor,
329                             const nsAString& resourceName,
330                             nsAString& nativeResourcePath);
331 };
332 
333 /**
334  * The watching thread logic.
335  *
336  * @return NS_OK if the watcher loop must be rescheduled, a failure code
337  *         if it must not.
338  */
RunInternal()339 nsresult NativeFileWatcherIOTask::RunInternal() {
340   // Contains the address of the |OVERLAPPED| structure passed
341   // to ReadDirectoryChangesW (used to check for |HANDLE| closing).
342   OVERLAPPED* overlappedStructure;
343 
344   // The number of bytes transferred by GetQueuedCompletionStatus
345   // (used to check for |HANDLE| closing).
346   DWORD transferredBytes = 0;
347 
348   // Will hold the |HANDLE| to the watched resource returned by
349   // GetQueuedCompletionStatus which generated the change events.
350   ULONG_PTR changedResourceHandle = 0;
351 
352   // Check for changes in the resource status by querying the
353   // |mIOCompletionPort| (blocking). GetQueuedCompletionStatus is always called
354   // before the first call to ReadDirectoryChangesW. This isn't a problem, since
355   // mIOCompletionPort is already a valid |HANDLE| even though it doesn't have
356   // any associated notification handles (through ReadDirectoryChangesW).
357   if (!GetQueuedCompletionStatus(mIOCompletionPort, &transferredBytes,
358                                  &changedResourceHandle, &overlappedStructure,
359                                  INFINITE)) {
360     // Ok, there was some error.
361     DWORD errCode = GetLastError();
362     switch (errCode) {
363       case ERROR_NOTIFY_ENUM_DIR: {
364         // There were too many changes and the notification buffer has
365         // overflowed. We dispatch the special value "*" and reschedule.
366         FILEWATCHERLOG(
367             "NativeFileWatcherIOTask::Run - Notification buffer has "
368             "overflowed");
369 
370         WatchedResourceDescriptor* changedRes =
371             mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle);
372 
373         nsresult rv =
374             DispatchChangeCallbacks(changedRes, NS_LITERAL_STRING("*"));
375         if (NS_FAILED(rv)) {
376           // We failed to dispatch the error callbacks. Something very
377           // bad happened to the main thread, so we bail out from the watcher
378           // thread.
379           FILEWATCHERLOG(
380               "NativeFileWatcherIOTask::Run - Failed to dispatch change "
381               "callbacks (%x).",
382               rv);
383           return rv;
384         }
385 
386         return NS_OK;
387       }
388       case ERROR_ABANDONED_WAIT_0:
389       case ERROR_INVALID_HANDLE: {
390         // If we reach this point, mIOCompletionPort was probably closed
391         // and we need to close this thread. This condition is identified
392         // by catching the  ERROR_INVALID_HANDLE error.
393         FILEWATCHERLOG(
394             "NativeFileWatcherIOTask::Run - The completion port was closed "
395             "(%x).",
396             errCode);
397         return NS_ERROR_ABORT;
398       }
399       case ERROR_OPERATION_ABORTED: {
400         // Some path was unwatched! That's not really an error, now it is safe
401         // to free the memory for the resource and call
402         // GetQueuedCompletionStatus again.
403         FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Path unwatched (%x).",
404                        errCode);
405 
406         WatchedResourceDescriptor* toFree =
407             mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle);
408 
409         if (toFree) {
410           // Take care of removing the resource and freeing the memory
411 
412           mWatchedResourcesByHandle.Remove((HANDLE)changedResourceHandle);
413 
414           // This last call eventually frees the memory
415           mWatchedResourcesByPath.Remove(toFree->mPath);
416         }
417 
418         return NS_OK;
419       }
420       default: {
421         // It should probably never get here, but it's better to be safe.
422         FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Unknown error (%x).",
423                        errCode);
424 
425         return NS_ERROR_FAILURE;
426       }
427     }
428   }
429 
430   // When an |HANDLE| associated to the completion I/O port is gracefully
431   // closed, GetQueuedCompletionStatus still may return a status update.
432   // Moreover, this can also be triggered when watching files on a network
433   // folder and losing the connection. That's an edge case we need to take care
434   // of for consistency by checking for (!transferredBytes &&
435   // overlappedStructure). See http://xania.org/200807/iocp
436   if (!transferredBytes && (overlappedStructure ||
437                             (!overlappedStructure && !changedResourceHandle))) {
438     // Note: if changedResourceHandle is nullptr as well, the wait on the
439     // Completion I/O was interrupted by a call to PostQueuedCompletionStatus
440     // with 0 transferred bytes and nullptr as |OVERLAPPED| and |HANDLE|. This
441     // is done to allow addPath and removePath to work on this thread.
442     return NS_OK;
443   }
444 
445   // Check to see which resource is changedResourceHandle.
446   WatchedResourceDescriptor* changedRes =
447       mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle);
448   MOZ_ASSERT(
449       changedRes,
450       "Could not find the changed resource in the list of watched ones.");
451 
452   // Parse the changes and notify the main thread.
453   const unsigned char* rawNotificationBuffer =
454       changedRes->mNotificationBuffer.get();
455 
456   while (true) {
457     FILE_NOTIFY_INFORMATION* notificationInfo =
458         (FILE_NOTIFY_INFORMATION*)rawNotificationBuffer;
459 
460     // FileNameLength is in bytes, but we need FileName length
461     // in characters, so divide it by sizeof(WCHAR).
462     nsAutoString resourceName(notificationInfo->FileName,
463                               notificationInfo->FileNameLength / sizeof(WCHAR));
464 
465     // Handle path normalisation using nsIFile.
466     nsString resourcePath;
467     nsresult rv = MakeResourcePath(changedRes, resourceName, resourcePath);
468     if (NS_SUCCEEDED(rv)) {
469       rv = DispatchChangeCallbacks(changedRes, resourcePath);
470       if (NS_FAILED(rv)) {
471         // Log that we failed to dispatch the change callbacks.
472         FILEWATCHERLOG(
473             "NativeFileWatcherIOTask::Run - Failed to dispatch change "
474             "callbacks (%x).",
475             rv);
476         return rv;
477       }
478     }
479 
480     if (!notificationInfo->NextEntryOffset) {
481       break;
482     }
483 
484     rawNotificationBuffer += notificationInfo->NextEntryOffset;
485   }
486 
487   // We need to keep watching for further changes.
488   nsresult rv = AddDirectoryToWatchList(changedRes);
489   if (NS_FAILED(rv)) {
490     // We failed to watch the folder.
491     if (rv == NS_ERROR_ABORT) {
492       // Log that we also failed to dispatch the error callbacks.
493       FILEWATCHERLOG(
494           "NativeFileWatcherIOTask::Run - Failed to watch %s and"
495           " to dispatch the related error callbacks",
496           changedRes->mPath.get());
497       return rv;
498     }
499   }
500 
501   return NS_OK;
502 }
503 
504 /**
505  * Wraps the watcher logic and takes care of rescheduling
506  * the watcher loop based on the return code of |RunInternal|
507  * in order to help with code readability.
508  *
509  * @return NS_OK or a failure error code from |NS_DispatchToCurrentThread|.
510  */
511 NS_IMETHODIMP
Run()512 NativeFileWatcherIOTask::Run() {
513   MOZ_ASSERT(!NS_IsMainThread());
514 
515   // We return immediately if |mShuttingDown| is true (see below for
516   // details about the shutdown protocol being followed).
517   if (mShuttingDown) {
518     return NS_OK;
519   }
520 
521   nsresult rv = RunInternal();
522   if (NS_FAILED(rv)) {
523     // A critical error occurred in the watcher loop, don't reschedule.
524     FILEWATCHERLOG(
525         "NativeFileWatcherIOTask::Run - Stopping the watcher loop (error %S)",
526         rv);
527 
528     // We log the error but return NS_OK instead: we don't want to
529     // propagate an exception through XPCOM.
530     return NS_OK;
531   }
532 
533   // No error occurred, reschedule.
534   return NS_DispatchToCurrentThread(this);
535 }
536 
537 /**
538  * Adds the resource to the watched list. This function is enqueued on the
539  * worker thread by NativeFileWatcherService::AddPath. All the errors are
540  * reported to the main thread using the error callback function mErrorCallback.
541  *
542  * @param pathToWatch
543  *        The path of the resource to watch for changes.
544  *
545  * @return NS_ERROR_FILE_NOT_FOUND if the path is invalid or does not exist.
546  *         Returns NS_ERROR_UNEXPECTED if OS |HANDLE|s are unexpectedly closed.
547  *         If the ReadDirectoryChangesW call fails, returns NS_ERROR_FAILURE,
548  *         otherwise NS_OK.
549  */
AddPathRunnableMethod(PathRunnablesParametersWrapper * aWrappedParameters)550 nsresult NativeFileWatcherIOTask::AddPathRunnableMethod(
551     PathRunnablesParametersWrapper* aWrappedParameters) {
552   MOZ_ASSERT(!NS_IsMainThread());
553 
554   nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(
555       aWrappedParameters);
556 
557   // We return immediately if |mShuttingDown| is true (see below for
558   // details about the shutdown protocol being followed).
559   if (mShuttingDown) {
560     return NS_OK;
561   }
562 
563   if (!wrappedParameters || !wrappedParameters->mChangeCallbackHandle) {
564     FILEWATCHERLOG(
565         "NativeFileWatcherIOTask::AddPathRunnableMethod - Invalid arguments.");
566     return NS_ERROR_NULL_POINTER;
567   }
568 
569   // Is aPathToWatch already being watched?
570   WatchedResourceDescriptor* watchedResource =
571       mWatchedResourcesByPath.Get(wrappedParameters->mPath);
572   if (watchedResource) {
573     // Append it to the hash tables.
574     AppendCallbacksToHashtables(watchedResource->mPath,
575                                 wrappedParameters->mChangeCallbackHandle,
576                                 wrappedParameters->mErrorCallbackHandle);
577 
578     return NS_OK;
579   }
580 
581   // Retrieve a file handle to associate with the completion port. Makes
582   // sure to request the appropriate rights (i.e. read files and list
583   // files contained in a folder). Note: the nullptr security flag prevents
584   // the |HANDLE| to be passed to child processes.
585   HANDLE resHandle = CreateFileW(
586       wrappedParameters->mPath.get(),
587       FILE_LIST_DIRECTORY,                                     // Access rights
588       FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,  // Share
589       nullptr,                                                 // Security flags
590       OPEN_EXISTING,  // Returns an handle only if the resource exists
591       FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
592       nullptr);  // Template file (only used when creating files)
593   if (resHandle == INVALID_HANDLE_VALUE) {
594     DWORD dwError = GetLastError();
595     nsresult rv;
596     if (dwError == ERROR_FILE_NOT_FOUND) {
597       rv = NS_ERROR_FILE_NOT_FOUND;
598     } else if (dwError == ERROR_ACCESS_DENIED) {
599       rv = NS_ERROR_FILE_ACCESS_DENIED;
600     } else {
601       rv = NS_ERROR_FAILURE;
602     }
603 
604     FILEWATCHERLOG(
605         "NativeFileWatcherIOTask::AddPathRunnableMethod - CreateFileW failed "
606         "(error %x) for %S.",
607         dwError, wrappedParameters->mPath.get());
608 
609     rv = ReportError(wrappedParameters->mErrorCallbackHandle, rv, dwError);
610     if (NS_FAILED(rv)) {
611       FILEWATCHERLOG(
612           "NativeFileWatcherIOTask::AddPathRunnableMethod - "
613           "Failed to dispatch the error callback (%x).",
614           rv);
615       return rv;
616     }
617 
618     // Error has already been reported through mErrorCallback.
619     return NS_OK;
620   }
621 
622   // Initialise the resource descriptor.
623   UniquePtr<WatchedResourceDescriptor> resourceDesc(
624       new WatchedResourceDescriptor(wrappedParameters->mPath, resHandle));
625 
626   // Associate the file with the previously opened completion port.
627   if (!CreateIoCompletionPort(resourceDesc->mResourceHandle, mIOCompletionPort,
628                               (ULONG_PTR)resourceDesc->mResourceHandle.get(),
629                               0)) {
630     DWORD dwError = GetLastError();
631 
632     FILEWATCHERLOG(
633         "NativeFileWatcherIOTask::AddPathRunnableMethod"
634         " - CreateIoCompletionPort failed (error %x) for %S.",
635         dwError, wrappedParameters->mPath.get());
636 
637     // This could fail because passed parameters could be invalid |HANDLE|s
638     // i.e. mIOCompletionPort was unexpectedly closed or failed.
639     nsresult rv = ReportError(wrappedParameters->mErrorCallbackHandle,
640                               NS_ERROR_UNEXPECTED, dwError);
641     if (NS_FAILED(rv)) {
642       FILEWATCHERLOG(
643           "NativeFileWatcherIOTask::AddPathRunnableMethod - "
644           "Failed to dispatch the error callback (%x).",
645           rv);
646       return rv;
647     }
648 
649     // Error has already been reported through mErrorCallback.
650     return NS_OK;
651   }
652 
653   // Append the callbacks to the hash tables. We do this now since
654   // AddDirectoryToWatchList could use the error callback, but we
655   // need to make sure to remove them if AddDirectoryToWatchList fails.
656   AppendCallbacksToHashtables(wrappedParameters->mPath,
657                               wrappedParameters->mChangeCallbackHandle,
658                               wrappedParameters->mErrorCallbackHandle);
659 
660   // We finally watch the resource for changes.
661   nsresult rv = AddDirectoryToWatchList(resourceDesc.get());
662   if (NS_SUCCEEDED(rv)) {
663     // Add the resource pointer to both indexes.
664     WatchedResourceDescriptor* resource = resourceDesc.release();
665     mWatchedResourcesByPath.Put(wrappedParameters->mPath, resource);
666     mWatchedResourcesByHandle.Put(resHandle, resource);
667 
668     // Dispatch the success callback.
669     nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle,
670                                 wrappedParameters->mPath);
671     if (NS_FAILED(rv)) {
672       FILEWATCHERLOG(
673           "NativeFileWatcherIOTask::AddPathRunnableMethod - "
674           "Failed to dispatch the success callback (%x).",
675           rv);
676       return rv;
677     }
678 
679     return NS_OK;
680   }
681 
682   // We failed to watch the folder. Remove the callbacks
683   // from the hash tables.
684   RemoveCallbacksFromHashtables(wrappedParameters->mPath,
685                                 wrappedParameters->mChangeCallbackHandle,
686                                 wrappedParameters->mErrorCallbackHandle);
687 
688   if (rv != NS_ERROR_ABORT) {
689     // Just don't add the descriptor to the watch list.
690     return NS_OK;
691   }
692 
693   // We failed to dispatch the error callbacks as well.
694   FILEWATCHERLOG(
695       "NativeFileWatcherIOTask::AddPathRunnableMethod - Failed to watch %s and"
696       " to dispatch the related error callbacks",
697       resourceDesc->mPath.get());
698 
699   return rv;
700 }
701 
702 /**
703  * Removes the path from the list of watched resources. Silently ignores the
704  * request if the path was not being watched.
705  *
706  * Remove Protocol:
707  *
708  * 1. Find the resource to unwatch through the provided path.
709  * 2. Remove the error and change callbacks from the list of callbacks
710  *    associated with the resource.
711  * 3. Remove the error and change callbacks from the callback hash maps.
712  * 4. If there are no more change callbacks for the resource, close
713  *    its file |HANDLE|. We do not free the buffer memory just yet, it's
714  *    still needed for the next call to GetQueuedCompletionStatus. That
715  *    memory will be freed in NativeFileWatcherIOTask::Run.
716  *
717  * @param aWrappedParameters
718  *        The structure containing the resource path, the error and change
719  * callback handles.
720  */
RemovePathRunnableMethod(PathRunnablesParametersWrapper * aWrappedParameters)721 nsresult NativeFileWatcherIOTask::RemovePathRunnableMethod(
722     PathRunnablesParametersWrapper* aWrappedParameters) {
723   MOZ_ASSERT(!NS_IsMainThread());
724 
725   nsAutoPtr<PathRunnablesParametersWrapper> wrappedParameters(
726       aWrappedParameters);
727 
728   // We return immediately if |mShuttingDown| is true (see below for
729   // details about the shutdown protocol being followed).
730   if (mShuttingDown) {
731     return NS_OK;
732   }
733 
734   if (!wrappedParameters || !wrappedParameters->mChangeCallbackHandle) {
735     return NS_ERROR_NULL_POINTER;
736   }
737 
738   WatchedResourceDescriptor* toRemove =
739       mWatchedResourcesByPath.Get(wrappedParameters->mPath);
740   if (!toRemove) {
741     // We are trying to remove a path which wasn't being watched. Silently
742     // ignore and dispatch the success callback.
743     nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle,
744                                 wrappedParameters->mPath);
745     if (NS_FAILED(rv)) {
746       FILEWATCHERLOG(
747           "NativeFileWatcherIOTask::RemovePathRunnableMethod - "
748           "Failed to dispatch the success callback (%x).",
749           rv);
750       return rv;
751     }
752     return NS_OK;
753   }
754 
755   ChangeCallbackArray* changeCallbackArray =
756       mChangeCallbacksTable.Get(toRemove->mPath);
757 
758   // This should always be valid.
759   MOZ_ASSERT(changeCallbackArray);
760 
761   bool removed = changeCallbackArray->RemoveElement(
762       wrappedParameters->mChangeCallbackHandle);
763   if (!removed) {
764     FILEWATCHERLOG(
765         "NativeFileWatcherIOTask::RemovePathRunnableMethod - Unable to remove "
766         "the change "
767         "callback from the change callback hash map for %S.",
768         wrappedParameters->mPath.get());
769     MOZ_CRASH();
770   }
771 
772   ErrorCallbackArray* errorCallbackArray =
773       mErrorCallbacksTable.Get(toRemove->mPath);
774 
775   MOZ_ASSERT(errorCallbackArray);
776 
777   removed = errorCallbackArray->RemoveElement(
778       wrappedParameters->mErrorCallbackHandle);
779   if (!removed) {
780     FILEWATCHERLOG(
781         "NativeFileWatcherIOTask::RemovePathRunnableMethod - Unable to remove "
782         "the error "
783         "callback from the error callback hash map for %S.",
784         wrappedParameters->mPath.get());
785     MOZ_CRASH();
786   }
787 
788   // If there are still callbacks left, keep the descriptor.
789   // We don't check for error callbacks since there's no point in keeping
790   // the descriptor if there are no change callbacks but some error callbacks.
791   if (changeCallbackArray->Length()) {
792     // Dispatch the success callback.
793     nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle,
794                                 wrappedParameters->mPath);
795     if (NS_FAILED(rv)) {
796       FILEWATCHERLOG(
797           "NativeFileWatcherIOTask::RemovePathRunnableMethod - "
798           "Failed to dispatch the success callback (%x).",
799           rv);
800       return rv;
801     }
802     return NS_OK;
803   }
804 
805   // In this runnable, we just cancel callbacks (see above) and I/O (see below).
806   // Resources are freed by the worker thread when GetQueuedCompletionStatus
807   // detects that a resource was removed from the watch list.
808   // Since when closing |HANDLE|s relative to watched resources
809   // GetQueuedCompletionStatus is notified one last time, it would end
810   // up referring to deallocated memory if we were to free the memory here.
811   // This happens because the worker IO is scheduled to watch the resources
812   // again once we complete executing this function.
813 
814   // Enforce CloseHandle/CancelIO by disposing the AutoCloseHandle. We don't
815   // remove the entry from mWatchedResourceBy* since the completion port might
816   // still be using the notification buffer. Entry remove is performed when
817   // handling ERROR_OPERATION_ABORTED in NativeFileWatcherIOTask::Run.
818   toRemove->mResourceHandle.dispose();
819 
820   // Dispatch the success callback.
821   nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle,
822                               wrappedParameters->mPath);
823   if (NS_FAILED(rv)) {
824     FILEWATCHERLOG(
825         "NativeFileWatcherIOTask::RemovePathRunnableMethod - "
826         "Failed to dispatch the success callback (%x).",
827         rv);
828     return rv;
829   }
830 
831   return NS_OK;
832 }
833 
834 /**
835  * Removes all the watched resources from the watch list and stops the
836  * watcher thread. Frees all the used resources.
837  */
DeactivateRunnableMethod()838 nsresult NativeFileWatcherIOTask::DeactivateRunnableMethod() {
839   MOZ_ASSERT(!NS_IsMainThread());
840 
841   // Remind users to manually remove the watches before quitting.
842   MOZ_ASSERT(!mWatchedResourcesByHandle.Count(),
843              "Clients of the nsINativeFileWatcher must remove "
844              "watches manually before quitting.");
845 
846   // Log any pending watch.
847   for (auto it = mWatchedResourcesByHandle.Iter(); !it.Done(); it.Next()) {
848     FILEWATCHERLOG(
849         "NativeFileWatcherIOTask::DeactivateRunnableMethod - "
850         "%S is still being watched.",
851         it.UserData()->mPath.get());
852   }
853 
854   // We return immediately if |mShuttingDown| is true (see below for
855   // details about the shutdown protocol being followed).
856   if (mShuttingDown) {
857     // If this happens, we are in a strange situation.
858     FILEWATCHERLOG(
859         "NativeFileWatcherIOTask::DeactivateRunnableMethod - We are already "
860         "shutting down.");
861     MOZ_CRASH();
862     return NS_OK;
863   }
864 
865   // Deactivate all the non-shutdown methods of this object.
866   mShuttingDown = true;
867 
868   // Remove all the elements from the index. Memory will be freed by
869   // calling Clear() on mWatchedResourcesByPath.
870   mWatchedResourcesByHandle.Clear();
871 
872   // Clear frees the memory associated with each element and clears the table.
873   // Since we are using Scoped |HANDLE|s, they get automatically closed as well.
874   mWatchedResourcesByPath.Clear();
875 
876   // Now that all the descriptors are closed, release the callback hahstables.
877   mChangeCallbacksTable.Clear();
878   mErrorCallbacksTable.Clear();
879 
880   // Close the IO completion port, eventually making
881   // the watcher thread exit from the watching loop.
882   if (mIOCompletionPort) {
883     if (!CloseHandle(mIOCompletionPort)) {
884       FILEWATCHERLOG(
885           "NativeFileWatcherIOTask::DeactivateRunnableMethod - "
886           "Failed to close the IO completion port HANDLE.");
887     }
888   }
889 
890   // Now we just need to reschedule a final call to Shutdown() back to the main
891   // thread.
892   RefPtr<NativeWatcherIOShutdownTask> shutdownRunnable =
893       new NativeWatcherIOShutdownTask();
894 
895   return NS_DispatchToMainThread(shutdownRunnable);
896 }
897 
898 /**
899  * Helper function to dispatch a change notification to all the registered
900  * callbacks.
901  * @param aResourceDescriptor
902  *        The resource descriptor.
903  * @param aChangedResource
904  *        The path of the changed resource.
905  * @return NS_OK if all the callbacks are dispatched correctly, a |nsresult|
906  *         error code otherwise.
907  */
DispatchChangeCallbacks(WatchedResourceDescriptor * aResourceDescriptor,const nsAString & aChangedResource)908 nsresult NativeFileWatcherIOTask::DispatchChangeCallbacks(
909     WatchedResourceDescriptor* aResourceDescriptor,
910     const nsAString& aChangedResource) {
911   MOZ_ASSERT(aResourceDescriptor);
912 
913   // Retrieve the change callbacks array.
914   ChangeCallbackArray* changeCallbackArray =
915       mChangeCallbacksTable.Get(aResourceDescriptor->mPath);
916 
917   // This should always be valid.
918   MOZ_ASSERT(changeCallbackArray);
919 
920   for (size_t i = 0; i < changeCallbackArray->Length(); i++) {
921     nsresult rv = ReportChange((*changeCallbackArray)[i], aChangedResource);
922     if (NS_FAILED(rv)) {
923       return rv;
924     }
925   }
926 
927   return NS_OK;
928 }
929 
930 /**
931  * Helper function to post a change runnable to the main thread.
932  *
933  * @param aOnChange
934  *        The change callback handle.
935  * @param aChangedResource
936  *        The resource name to dispatch thorough the change callback.
937  *
938  * @return NS_OK if the callback is dispatched correctly.
939  */
ReportChange(const nsMainThreadPtrHandle<nsINativeFileWatcherCallback> & aOnChange,const nsAString & aChangedResource)940 nsresult NativeFileWatcherIOTask::ReportChange(
941     const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange,
942     const nsAString& aChangedResource) {
943   RefPtr<WatchedChangeEvent> changeRunnable =
944       new WatchedChangeEvent(aOnChange, aChangedResource);
945   return NS_DispatchToMainThread(changeRunnable);
946 }
947 
948 /**
949  * Helper function to dispatch a error notification to all the registered
950  * callbacks.
951  * @param aResourceDescriptor
952  *        The resource descriptor.
953  * @param anError
954  *        The error to dispatch thorough the error callback.
955  * @param anOSError
956  *        An OS specific error code to send with the callback.
957  * @return NS_OK if all the callbacks are dispatched correctly, a |nsresult|
958  * error code otherwise.
959  */
DispatchErrorCallbacks(WatchedResourceDescriptor * aResourceDescriptor,nsresult anError,DWORD anOSError)960 nsresult NativeFileWatcherIOTask::DispatchErrorCallbacks(
961     WatchedResourceDescriptor* aResourceDescriptor, nsresult anError,
962     DWORD anOSError) {
963   MOZ_ASSERT(aResourceDescriptor);
964 
965   // Retrieve the error callbacks array.
966   ErrorCallbackArray* errorCallbackArray =
967       mErrorCallbacksTable.Get(aResourceDescriptor->mPath);
968 
969   // This must be valid.
970   MOZ_ASSERT(errorCallbackArray);
971 
972   for (size_t i = 0; i < errorCallbackArray->Length(); i++) {
973     nsresult rv = ReportError((*errorCallbackArray)[i], anError, anOSError);
974     if (NS_FAILED(rv)) {
975       return rv;
976     }
977   }
978 
979   return NS_OK;
980 }
981 
982 /**
983  * Helper function to post an error runnable to the main thread.
984  *
985  * @param aOnError
986  *        The error callback handle.
987  * @param anError
988  *        The error to dispatch thorough the error callback.
989  * @param anOSError
990  *        An OS specific error code to send with the callback.
991  *
992  * @return NS_OK if the callback is dispatched correctly.
993  */
ReportError(const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> & aOnError,nsresult anError,DWORD anOSError)994 nsresult NativeFileWatcherIOTask::ReportError(
995     const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError,
996     nsresult anError, DWORD anOSError) {
997   RefPtr<WatchedErrorEvent> errorRunnable =
998       new WatchedErrorEvent(aOnError, anError, anOSError);
999   return NS_DispatchToMainThread(errorRunnable);
1000 }
1001 
1002 /**
1003  * Helper function to post a success runnable to the main thread.
1004  *
1005  * @param aOnSuccess
1006  *        The success callback handle.
1007  * @param aResource
1008  *        The resource name to dispatch thorough the success callback.
1009  *
1010  * @return NS_OK if the callback is dispatched correctly.
1011  */
ReportSuccess(const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> & aOnSuccess,const nsAString & aResource)1012 nsresult NativeFileWatcherIOTask::ReportSuccess(
1013     const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>&
1014         aOnSuccess,
1015     const nsAString& aResource) {
1016   RefPtr<WatchedSuccessEvent> successRunnable =
1017       new WatchedSuccessEvent(aOnSuccess, aResource);
1018   return NS_DispatchToMainThread(successRunnable);
1019 }
1020 
1021 /**
1022  * Instructs the OS to report the changes concerning the directory of interest.
1023  *
1024  * @param aDirectoryDescriptor
1025  *        A |WatchedResourceDescriptor| instance describing the directory to
1026  *        watch.
1027  * @param aDispatchErrorCode
1028  *        If |ReadDirectoryChangesW| fails and dispatching an error callback to
1029  *        the main thread fails as well, the error code is stored here. If the
1030  *        OS API call does not fail, it gets set to NS_OK.
1031  * @return |true| if |ReadDirectoryChangesW| returned no error, |false|
1032  *         otherwise.
1033  */
AddDirectoryToWatchList(WatchedResourceDescriptor * aDirectoryDescriptor)1034 nsresult NativeFileWatcherIOTask::AddDirectoryToWatchList(
1035     WatchedResourceDescriptor* aDirectoryDescriptor) {
1036   MOZ_ASSERT(!mShuttingDown);
1037 
1038   DWORD dwPlaceholder;
1039   // Tells the OS to watch out on mResourceHandle for the changes specified
1040   // with the FILE_NOTIFY_* flags. We monitor the creation, renaming and
1041   // deletion of a file (FILE_NOTIFY_CHANGE_FILE_NAME), changes to the last
1042   // modification time (FILE_NOTIFY_CHANGE_LAST_WRITE) and the creation and
1043   // deletion of a folder (FILE_NOTIFY_CHANGE_DIR_NAME). Moreover, when you
1044   // first call this function, the system allocates a buffer to store change
1045   // information for the watched directory.
1046   if (!ReadDirectoryChangesW(
1047           aDirectoryDescriptor->mResourceHandle,
1048           aDirectoryDescriptor->mNotificationBuffer.get(),
1049           NOTIFICATION_BUFFER_SIZE,
1050           true,  // watch subtree (recurse)
1051           FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME |
1052               FILE_NOTIFY_CHANGE_DIR_NAME,
1053           &dwPlaceholder, &aDirectoryDescriptor->mOverlappedInfo, nullptr)) {
1054     // NOTE: GetLastError() could return ERROR_INVALID_PARAMETER if the buffer
1055     // length is greater than 64 KB and the application is monitoring a
1056     // directory over the network. The same error could be returned when trying
1057     // to watch a file instead of a directory. It could return ERROR_NOACCESS if
1058     // the buffer is not aligned on a DWORD boundary.
1059     DWORD dwError = GetLastError();
1060 
1061     FILEWATCHERLOG(
1062         "NativeFileWatcherIOTask::AddDirectoryToWatchList "
1063         " - ReadDirectoryChangesW failed (error %x) for %S.",
1064         dwError, aDirectoryDescriptor->mPath.get());
1065 
1066     nsresult rv =
1067         DispatchErrorCallbacks(aDirectoryDescriptor, NS_ERROR_FAILURE, dwError);
1068     if (NS_FAILED(rv)) {
1069       // That's really bad. We failed to watch the directory and failed to
1070       // dispatch the error callbacks.
1071       return NS_ERROR_ABORT;
1072     }
1073 
1074     // We failed to watch the directory, but we correctly dispatched the error
1075     // callbacks.
1076     return NS_ERROR_FAILURE;
1077   }
1078 
1079   return NS_OK;
1080 }
1081 
1082 /**
1083  * Appends the change and error callbacks to their respective hash tables.
1084  * It also checks if the callbacks are already attached to them.
1085  * @param aPath
1086  *        The watched directory path.
1087  * @param aOnChangeHandle
1088  *        The callback to invoke when a change is detected.
1089  * @param aOnErrorHandle
1090  *        The callback to invoke when an error is detected.
1091  */
AppendCallbacksToHashtables(const nsAString & aPath,const nsMainThreadPtrHandle<nsINativeFileWatcherCallback> & aOnChangeHandle,const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> & aOnErrorHandle)1092 void NativeFileWatcherIOTask::AppendCallbacksToHashtables(
1093     const nsAString& aPath,
1094     const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChangeHandle,
1095     const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>&
1096         aOnErrorHandle) {
1097   // First check to see if we've got an entry already.
1098   ChangeCallbackArray* callbacksArray = mChangeCallbacksTable.Get(aPath);
1099   if (!callbacksArray) {
1100     // We don't have an entry. Create an array and put it into the hash table.
1101     callbacksArray = new ChangeCallbackArray();
1102     mChangeCallbacksTable.Put(aPath, callbacksArray);
1103   }
1104 
1105   // We do have an entry for that path. Check to see if the callback is
1106   // already there.
1107   ChangeCallbackArray::index_type changeCallbackIndex =
1108       callbacksArray->IndexOf(aOnChangeHandle);
1109 
1110   // If the callback is not attached to the descriptor, append it.
1111   if (changeCallbackIndex == ChangeCallbackArray::NoIndex) {
1112     callbacksArray->AppendElement(aOnChangeHandle);
1113   }
1114 
1115   // Same thing for the error callback.
1116   ErrorCallbackArray* errorCallbacksArray = mErrorCallbacksTable.Get(aPath);
1117   if (!errorCallbacksArray) {
1118     // We don't have an entry. Create an array and put it into the hash table.
1119     errorCallbacksArray = new ErrorCallbackArray();
1120     mErrorCallbacksTable.Put(aPath, errorCallbacksArray);
1121   }
1122 
1123   ErrorCallbackArray::index_type errorCallbackIndex =
1124       errorCallbacksArray->IndexOf(aOnErrorHandle);
1125 
1126   if (errorCallbackIndex == ErrorCallbackArray::NoIndex) {
1127     errorCallbacksArray->AppendElement(aOnErrorHandle);
1128   }
1129 }
1130 
1131 /**
1132  * Removes the change and error callbacks from their respective hash tables.
1133  * @param aPath
1134  *        The watched directory path.
1135  * @param aOnChangeHandle
1136  *        The change callback to remove.
1137  * @param aOnErrorHandle
1138  *        The error callback to remove.
1139  */
RemoveCallbacksFromHashtables(const nsAString & aPath,const nsMainThreadPtrHandle<nsINativeFileWatcherCallback> & aOnChangeHandle,const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> & aOnErrorHandle)1140 void NativeFileWatcherIOTask::RemoveCallbacksFromHashtables(
1141     const nsAString& aPath,
1142     const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChangeHandle,
1143     const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>&
1144         aOnErrorHandle) {
1145   // Find the change callback array for |aPath|.
1146   ChangeCallbackArray* callbacksArray = mChangeCallbacksTable.Get(aPath);
1147   if (callbacksArray) {
1148     // Remove the change callback.
1149     callbacksArray->RemoveElement(aOnChangeHandle);
1150   }
1151 
1152   // Find the error callback array for |aPath|.
1153   ErrorCallbackArray* errorCallbacksArray = mErrorCallbacksTable.Get(aPath);
1154   if (errorCallbacksArray) {
1155     // Remove the error callback.
1156     errorCallbacksArray->RemoveElement(aOnErrorHandle);
1157   }
1158 }
1159 
1160 /**
1161  * Creates a string representing the native path for the changed resource.
1162  * It appends the resource name to the path of the changed descriptor by
1163  * using nsIFile.
1164  * @param changedDescriptor
1165  *        The descriptor of the watched resource.
1166  * @param resourceName
1167  *        The resource which triggered the change.
1168  * @param nativeResourcePath
1169  *        The full path to the changed resource.
1170  * @return NS_OK if nsIFile succeeded in building the path.
1171  */
MakeResourcePath(WatchedResourceDescriptor * changedDescriptor,const nsAString & resourceName,nsAString & nativeResourcePath)1172 nsresult NativeFileWatcherIOTask::MakeResourcePath(
1173     WatchedResourceDescriptor* changedDescriptor, const nsAString& resourceName,
1174     nsAString& nativeResourcePath) {
1175   nsCOMPtr<nsIFile> localPath(do_CreateInstance("@mozilla.org/file/local;1"));
1176   if (!localPath) {
1177     FILEWATCHERLOG(
1178         "NativeFileWatcherIOTask::MakeResourcePath - Failed to create a "
1179         "nsIFile instance.");
1180     return NS_ERROR_FAILURE;
1181   }
1182 
1183   nsresult rv = localPath->InitWithPath(changedDescriptor->mPath);
1184   if (NS_FAILED(rv)) {
1185     FILEWATCHERLOG(
1186         "NativeFileWatcherIOTask::MakeResourcePath - Failed to init nsIFile "
1187         "with %S (%x).",
1188         changedDescriptor->mPath.get(), rv);
1189     return rv;
1190   }
1191 
1192   rv = localPath->AppendRelativePath(resourceName);
1193   if (NS_FAILED(rv)) {
1194     FILEWATCHERLOG(
1195         "NativeFileWatcherIOTask::MakeResourcePath - Failed to append to %S "
1196         "(%x).",
1197         changedDescriptor->mPath.get(), rv);
1198     return rv;
1199   }
1200 
1201   rv = localPath->GetPath(nativeResourcePath);
1202   if (NS_FAILED(rv)) {
1203     FILEWATCHERLOG(
1204         "NativeFileWatcherIOTask::MakeResourcePath - Failed to get native path "
1205         "from nsIFile (%x).",
1206         rv);
1207     return rv;
1208   }
1209 
1210   return NS_OK;
1211 }
1212 
1213 }  // namespace
1214 
1215 // The NativeFileWatcherService component
1216 
1217 NS_IMPL_ISUPPORTS(NativeFileWatcherService, nsINativeFileWatcherService,
1218                   nsIObserver);
1219 
NativeFileWatcherService()1220 NativeFileWatcherService::NativeFileWatcherService() {}
1221 
~NativeFileWatcherService()1222 NativeFileWatcherService::~NativeFileWatcherService() {}
1223 
1224 /**
1225  * Sets the required resources and starts the watching IO thread.
1226  *
1227  * @return NS_OK if there was no error with thread creation and execution.
1228  */
Init()1229 nsresult NativeFileWatcherService::Init() {
1230   // Creates an IO completion port and allows at most 2 thread to access it
1231   // concurrently.
1232   AutoCloseHandle completionPort(
1233       CreateIoCompletionPort(INVALID_HANDLE_VALUE,  // FileHandle
1234                              nullptr,               // ExistingCompletionPort
1235                              0,                     // CompletionKey
1236                              2));                   // NumberOfConcurrentThreads
1237   if (!completionPort) {
1238     return NS_ERROR_FAILURE;
1239   }
1240 
1241   // Add an observer for the shutdown.
1242   nsCOMPtr<nsIObserverService> observerService =
1243       mozilla::services::GetObserverService();
1244   if (!observerService) {
1245     return NS_ERROR_FAILURE;
1246   }
1247 
1248   observerService->AddObserver(this, "xpcom-shutdown-threads", false);
1249 
1250   // Start the IO worker thread.
1251   mWorkerIORunnable = new NativeFileWatcherIOTask(completionPort);
1252   nsresult rv = NS_NewNamedThread("FileWatcher IO", getter_AddRefs(mIOThread),
1253                                   mWorkerIORunnable);
1254   if (NS_FAILED(rv)) {
1255     FILEWATCHERLOG(
1256         "NativeFileWatcherIOTask::Init - Unable to create and dispatch the "
1257         "worker thread (%x).",
1258         rv);
1259     return rv;
1260   }
1261 
1262   mIOCompletionPort = completionPort.forget();
1263 
1264   return NS_OK;
1265 }
1266 
1267 /**
1268  * Watches a path for changes: monitors the creations, name changes and
1269  * content changes to the files contained in the watched path.
1270  *
1271  * @param aPathToWatch
1272  *        The path of the resource to watch for changes.
1273  * @param aOnChange
1274  *        The callback to invoke when a change is detected.
1275  * @param aOnError
1276  *        The optional callback to invoke when there's an error.
1277  * @param aOnSuccess
1278  *        The optional callback to invoke when the file watcher starts
1279  *        watching the resource for changes.
1280  *
1281  * @return NS_OK or NS_ERROR_NOT_INITIALIZED if the instance was not
1282  * initialized. Other errors are reported by the error callback function.
1283  */
1284 NS_IMETHODIMP
AddPath(const nsAString & aPathToWatch,nsINativeFileWatcherCallback * aOnChange,nsINativeFileWatcherErrorCallback * aOnError,nsINativeFileWatcherSuccessCallback * aOnSuccess)1285 NativeFileWatcherService::AddPath(
1286     const nsAString& aPathToWatch, nsINativeFileWatcherCallback* aOnChange,
1287     nsINativeFileWatcherErrorCallback* aOnError,
1288     nsINativeFileWatcherSuccessCallback* aOnSuccess) {
1289   // Make sure the instance was initialized.
1290   if (!mIOThread) {
1291     return NS_ERROR_NOT_INITIALIZED;
1292   }
1293 
1294   // Be sure a valid change callback was passed.
1295   if (!aOnChange) {
1296     return NS_ERROR_NULL_POINTER;
1297   }
1298 
1299   nsMainThreadPtrHandle<nsINativeFileWatcherCallback> changeCallbackHandle(
1300       new nsMainThreadPtrHolder<nsINativeFileWatcherCallback>(
1301           "nsINativeFileWatcherCallback", aOnChange));
1302 
1303   nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> errorCallbackHandle(
1304       new nsMainThreadPtrHolder<nsINativeFileWatcherErrorCallback>(
1305           "nsINativeFileWatcherErrorCallback", aOnError));
1306 
1307   nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>
1308       successCallbackHandle(
1309           new nsMainThreadPtrHolder<nsINativeFileWatcherSuccessCallback>(
1310               "nsINativeFileWatcherSuccessCallback", aOnSuccess));
1311 
1312   // Wrap the path and the callbacks in order to pass them using
1313   // NewRunnableMethod.
1314   UniquePtr<PathRunnablesParametersWrapper> wrappedCallbacks(
1315       new PathRunnablesParametersWrapper(aPathToWatch, changeCallbackHandle,
1316                                          errorCallbackHandle,
1317                                          successCallbackHandle));
1318 
1319   // Since this function does a bit of I/O stuff , run it in the IO thread.
1320   nsresult rv = mIOThread->Dispatch(
1321       NewRunnableMethod<PathRunnablesParametersWrapper*>(
1322           "NativeFileWatcherIOTask::AddPathRunnableMethod",
1323           static_cast<NativeFileWatcherIOTask*>(mWorkerIORunnable.get()),
1324           &NativeFileWatcherIOTask::AddPathRunnableMethod,
1325           wrappedCallbacks.get()),
1326       nsIEventTarget::DISPATCH_NORMAL);
1327   if (NS_FAILED(rv)) {
1328     return rv;
1329   }
1330 
1331   // Since the dispatch succeeded, we let the runnable own the pointer.
1332   Unused << wrappedCallbacks.release();
1333 
1334   WakeUpWorkerThread();
1335 
1336   return NS_OK;
1337 }
1338 
1339 /**
1340  * Removes the path from the list of watched resources. Silently ignores the
1341  * request if the path was not being watched or the callbacks were not
1342  * registered.
1343  *
1344  * @param aPathToRemove
1345  *        The path of the resource to remove from the watch list.
1346  * @param aOnChange
1347  *        The callback to invoke when a change is detected.
1348  * @param aOnError
1349  *        The optionally registered callback invoked when there's an error.
1350  * @param aOnSuccess
1351  *        The optional callback to invoke when the file watcher stops
1352  *        watching the resource for changes.
1353  *
1354  * @return NS_OK or NS_ERROR_NOT_INITIALIZED if the instance was not
1355  *         initialized.  Other errors are reported by the error callback
1356  *         function.
1357  */
1358 NS_IMETHODIMP
RemovePath(const nsAString & aPathToRemove,nsINativeFileWatcherCallback * aOnChange,nsINativeFileWatcherErrorCallback * aOnError,nsINativeFileWatcherSuccessCallback * aOnSuccess)1359 NativeFileWatcherService::RemovePath(
1360     const nsAString& aPathToRemove, nsINativeFileWatcherCallback* aOnChange,
1361     nsINativeFileWatcherErrorCallback* aOnError,
1362     nsINativeFileWatcherSuccessCallback* aOnSuccess) {
1363   // Make sure the instance was initialized.
1364   if (!mIOThread) {
1365     return NS_ERROR_NOT_INITIALIZED;
1366   }
1367 
1368   // Be sure a valid change callback was passed.
1369   if (!aOnChange) {
1370     return NS_ERROR_NULL_POINTER;
1371   }
1372 
1373   nsMainThreadPtrHandle<nsINativeFileWatcherCallback> changeCallbackHandle(
1374       new nsMainThreadPtrHolder<nsINativeFileWatcherCallback>(
1375           "nsINativeFileWatcherCallback", aOnChange));
1376 
1377   nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> errorCallbackHandle(
1378       new nsMainThreadPtrHolder<nsINativeFileWatcherErrorCallback>(
1379           "nsINativeFileWatcherErrorCallback", aOnError));
1380 
1381   nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>
1382       successCallbackHandle(
1383           new nsMainThreadPtrHolder<nsINativeFileWatcherSuccessCallback>(
1384               "nsINativeFileWatcherSuccessCallback", aOnSuccess));
1385 
1386   // Wrap the path and the callbacks in order to pass them using
1387   // NewRunnableMethod.
1388   UniquePtr<PathRunnablesParametersWrapper> wrappedCallbacks(
1389       new PathRunnablesParametersWrapper(aPathToRemove, changeCallbackHandle,
1390                                          errorCallbackHandle,
1391                                          successCallbackHandle));
1392 
1393   // Since this function does a bit of I/O stuff, run it in the IO thread.
1394   nsresult rv = mIOThread->Dispatch(
1395       NewRunnableMethod<PathRunnablesParametersWrapper*>(
1396           "NativeFileWatcherIOTask::RemovePathRunnableMethod",
1397           static_cast<NativeFileWatcherIOTask*>(mWorkerIORunnable.get()),
1398           &NativeFileWatcherIOTask::RemovePathRunnableMethod,
1399           wrappedCallbacks.get()),
1400       nsIEventTarget::DISPATCH_NORMAL);
1401   if (NS_FAILED(rv)) {
1402     return rv;
1403   }
1404 
1405   // Since the dispatch succeeded, we let the runnable own the pointer.
1406   Unused << wrappedCallbacks.release();
1407 
1408   WakeUpWorkerThread();
1409 
1410   return NS_OK;
1411 }
1412 
1413 /**
1414  * Removes all the watched resources from the watch list and stops the
1415  * watcher thread. Frees all the used resources.
1416  *
1417  * To avoid race conditions, we need a Shutdown Protocol:
1418  *
1419  * 1. [MainThread]
1420  *    When the "xpcom-shutdown-threads" event is detected, Uninit() gets called.
1421  * 2. [MainThread]
1422  *    Uninit sends DeactivateRunnableMethod() to the WorkerThread.
1423  * 3. [WorkerThread]
1424  *    DeactivateRunnableMethod makes it clear to other methods that shutdown is
1425  *    in progress, stops the IO completion port wait and schedules the rest of
1426  * the deactivation for after every currently pending method call is complete.
1427  */
Uninit()1428 nsresult NativeFileWatcherService::Uninit() {
1429   // Make sure the instance was initialized (and not de-initialized yet).
1430   if (!mIOThread) {
1431     return NS_OK;
1432   }
1433 
1434   // We need to be sure that there will be no calls to 'mIOThread' once we have
1435   // entered 'Uninit()', even if we exit due to an error.
1436   nsCOMPtr<nsIThread> ioThread;
1437   ioThread.swap(mIOThread);
1438 
1439   // Since this function does a bit of I/O stuff (close file handle), run it
1440   // in the IO thread.
1441   nsresult rv = ioThread->Dispatch(
1442       NewRunnableMethod(
1443           "NativeFileWatcherIOTask::DeactivateRunnableMethod",
1444           static_cast<NativeFileWatcherIOTask*>(mWorkerIORunnable.get()),
1445           &NativeFileWatcherIOTask::DeactivateRunnableMethod),
1446       nsIEventTarget::DISPATCH_NORMAL);
1447   if (NS_FAILED(rv)) {
1448     return rv;
1449   }
1450 
1451   WakeUpWorkerThread();
1452 
1453   return NS_OK;
1454 }
1455 
1456 /**
1457  * Tells |NativeFileWatcherIOTask| to quit and to reschedule itself in order to
1458  * execute the other runnables enqueued in the worker tread.
1459  * This works by posting a bogus event to the blocking
1460  * |GetQueuedCompletionStatus| call in |NativeFileWatcherIOTask::Run()|.
1461  */
WakeUpWorkerThread()1462 void NativeFileWatcherService::WakeUpWorkerThread() {
1463   // The last 3 parameters represent the number of transferred bytes, the
1464   // changed resource |HANDLE| and the address of the |OVERLAPPED| structure
1465   // passed to GetQueuedCompletionStatus: we set them to nullptr so that we can
1466   // recognize that we requested an interruption from the Worker thread.
1467   PostQueuedCompletionStatus(mIOCompletionPort, 0, 0, nullptr);
1468 }
1469 
1470 /**
1471  * This method is used to catch the "xpcom-shutdown-threads" event in order
1472  * to shutdown this service when closing the application.
1473  */
1474 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)1475 NativeFileWatcherService::Observe(nsISupports* aSubject, const char* aTopic,
1476                                   const char16_t* aData) {
1477   MOZ_ASSERT(NS_IsMainThread());
1478 
1479   if (!strcmp("xpcom-shutdown-threads", aTopic)) {
1480     DebugOnly<nsresult> rv = Uninit();
1481     MOZ_ASSERT(NS_SUCCEEDED(rv));
1482     return NS_OK;
1483   }
1484 
1485   MOZ_ASSERT(false, "NativeFileWatcherService got an unexpected topic!");
1486 
1487   return NS_ERROR_UNEXPECTED;
1488 }
1489 
1490 }  // namespace mozilla
1491