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