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