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::__anond96dcf260111::AutoCloseHandleTraits153 static type empty() { return INVALID_HANDLE_VALUE; }
releasemozilla::__anond96dcf260111::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::__anond96dcf260111::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::__anond96dcf260111::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