/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * Native implementation of Watcher operations. */ #include "NativeFileWatcherWin.h" #include "mozilla/Services.h" #include "mozilla/UniquePtr.h" #include "nsClassHashtable.h" #include "nsDataHashtable.h" #include "nsILocalFile.h" #include "nsIObserverService.h" #include "nsProxyRelease.h" #include "nsTArray.h" #include "mozilla/Logging.h" namespace mozilla { // Enclose everything which is not exported in an anonymous namespace. namespace { /** * An event used to notify the main thread when an error happens. */ class WatchedErrorEvent final : public Runnable { public: /** * @param aOnError The passed error callback. * @param aError The |nsresult| error value. * @param osError The error returned by GetLastError(). */ WatchedErrorEvent(const nsMainThreadPtrHandle& aOnError, const nsresult& anError, const DWORD& osError) : mOnError(aOnError) , mError(anError) { MOZ_ASSERT(!NS_IsMainThread()); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); // Make sure we wrap a valid callback since it's not mandatory to provide // one when watching a resource. if (mOnError) { (void)mOnError->Complete(mError, mOsError); } return NS_OK; } private: nsMainThreadPtrHandle mOnError; nsresult mError; DWORD mOsError; }; /** * An event used to notify the main thread when an operation is successful. */ class WatchedSuccessEvent final : public Runnable { public: /** * @param aOnSuccess The passed success callback. * @param aResourcePath * The path of the resource for which this event was generated. */ WatchedSuccessEvent(const nsMainThreadPtrHandle& aOnSuccess, const nsAString& aResourcePath) : mOnSuccess(aOnSuccess) , mResourcePath(aResourcePath) { MOZ_ASSERT(!NS_IsMainThread()); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); // Make sure we wrap a valid callback since it's not mandatory to provide // one when watching a resource. if (mOnSuccess) { (void)mOnSuccess->Complete(mResourcePath); } return NS_OK; } private: nsMainThreadPtrHandle mOnSuccess; nsString mResourcePath; }; /** * An event used to notify the main thread of a change in a watched * resource. */ class WatchedChangeEvent final : public Runnable { public: /** * @param aOnChange The passed change callback. * @param aChangedResource The name of the changed resource. */ WatchedChangeEvent(const nsMainThreadPtrHandle& aOnChange, const nsAString& aChangedResource) : mOnChange(aOnChange) , mChangedResource(aChangedResource) { MOZ_ASSERT(!NS_IsMainThread()); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); // The second parameter is reserved for future uses: we use 0 as a placeholder. (void)mOnChange->Changed(mChangedResource, 0); return NS_OK; } private: nsMainThreadPtrHandle mOnChange; nsString mChangedResource; }; static mozilla::LazyLogModule gNativeWatcherPRLog("NativeFileWatcherService"); #define FILEWATCHERLOG(...) MOZ_LOG(gNativeWatcherPRLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) // The number of notifications to store within WatchedResourceDescriptor:mNotificationBuffer. // If the buffer overflows, its contents are discarded and a change callback is dispatched // with "*" as changed path. const unsigned int WATCHED_RES_MAXIMUM_NOTIFICATIONS = 100; // The size, in bytes, of the notification buffer used to store the changes notifications // for each watched resource. const size_t NOTIFICATION_BUFFER_SIZE = WATCHED_RES_MAXIMUM_NOTIFICATIONS * sizeof(FILE_NOTIFY_INFORMATION); /** * AutoCloseHandle is a RAII wrapper for Windows |HANDLE|s */ struct AutoCloseHandleTraits { typedef HANDLE type; static type empty() { return INVALID_HANDLE_VALUE; } static void release(type anHandle) { if (anHandle != INVALID_HANDLE_VALUE) { // If CancelIo is called on an |HANDLE| not yet associated to a Completion I/O // it simply does nothing. (void)CancelIo(anHandle); (void)CloseHandle(anHandle); } } }; typedef Scoped AutoCloseHandle; // Define these callback array types to make the code easier to read. typedef nsTArray> ChangeCallbackArray; typedef nsTArray> ErrorCallbackArray; /** * A structure to keep track of the information related to a * watched resource. */ struct WatchedResourceDescriptor { // The path on the file system of the watched resource. nsString mPath; // A buffer containing the latest notifications received for the resource. // UniquePtr cannot be used as the structure // contains a variable length field (FileName). UniquePtr mNotificationBuffer; // Used to hold information for the asynchronous ReadDirectoryChangesW call // (does not need to be closed as it is not an |HANDLE|). OVERLAPPED mOverlappedInfo; // The OS handle to the watched resource. AutoCloseHandle mResourceHandle; WatchedResourceDescriptor(const nsAString& aPath, const HANDLE anHandle) : mPath(aPath) , mResourceHandle(anHandle) { memset(&mOverlappedInfo, 0, sizeof(OVERLAPPED)); mNotificationBuffer.reset(new unsigned char[NOTIFICATION_BUFFER_SIZE]); } }; /** * A structure used to pass the callbacks to the AddPathRunnableMethod() and * RemovePathRunnableMethod(). */ struct PathRunnablesParametersWrapper { nsString mPath; nsMainThreadPtrHandle mChangeCallbackHandle; nsMainThreadPtrHandle mErrorCallbackHandle; nsMainThreadPtrHandle mSuccessCallbackHandle; PathRunnablesParametersWrapper( const nsAString& aPath, const nsMainThreadPtrHandle& aOnChange, const nsMainThreadPtrHandle& aOnError, const nsMainThreadPtrHandle& aOnSuccess) : mPath(aPath) , mChangeCallbackHandle(aOnChange) , mErrorCallbackHandle(aOnError) , mSuccessCallbackHandle(aOnSuccess) { } }; /** * This runnable is dispatched to the main thread in order to safely * shutdown the worker thread. */ class NativeWatcherIOShutdownTask : public Runnable { public: NativeWatcherIOShutdownTask() : mWorkerThread(do_GetCurrentThread()) { MOZ_ASSERT(!NS_IsMainThread()); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); mWorkerThread->Shutdown(); return NS_OK; } private: nsCOMPtr mWorkerThread; }; /** * This runnable is dispatched from the main thread to get the notifications of the * changes in the watched resources by continuously calling the blocking function * GetQueuedCompletionStatus. This function queries the status of the Completion I/O * port initialized in the main thread. The watched resources are registered to the * completion I/O port when calling |addPath|. * * Instead of using a loop within the Run() method, the Runnable reschedules itself * by issuing a NS_DispatchToCurrentThread(this) before exiting. This is done to allow * the execution of other runnables enqueued within the thread task queue. */ class NativeFileWatcherIOTask : public Runnable { public: NativeFileWatcherIOTask(HANDLE aIOCompletionPort) : mIOCompletionPort(aIOCompletionPort) , mShuttingDown(false) { } NS_IMETHOD Run(); nsresult AddPathRunnableMethod(PathRunnablesParametersWrapper* aWrappedParameters); nsresult RemovePathRunnableMethod(PathRunnablesParametersWrapper* aWrappedParameters); nsresult DeactivateRunnableMethod(); private: // Maintain 2 indexes - one by resource path, one by resource |HANDLE|. // Since |HANDLE| is basically a typedef to void*, we use nsVoidPtrHashKey to // compute the hashing key. We need 2 indexes in order to quickly look up the // changed resource in the Worker Thread. // The objects are not ref counted and get destroyed by mWatchedResourcesByPath // on NativeFileWatcherService::Destroy or in NativeFileWatcherService::RemovePath. nsClassHashtable mWatchedResourcesByPath; nsDataHashtable mWatchedResourcesByHandle; // The same callback can be associated to multiple watches so we need to keep // them alive as long as there is a watch using them. We create two hashtables // to map directory names to lists of nsMainThreadPtr. nsClassHashtable mChangeCallbacksTable; nsClassHashtable mErrorCallbacksTable; // We hold a copy of the completion port |HANDLE|, which is owned by the main thread. HANDLE mIOCompletionPort; // Other methods need to know that a shutdown is in progress. bool mShuttingDown; nsresult RunInternal(); nsresult DispatchChangeCallbacks(WatchedResourceDescriptor* aResourceDescriptor, const nsAString& aChangedResource); nsresult ReportChange( const nsMainThreadPtrHandle& aOnChange, const nsAString& aChangedResource); nsresult DispatchErrorCallbacks(WatchedResourceDescriptor* aResourceDescriptor, nsresult anError, DWORD anOSError); nsresult ReportError( const nsMainThreadPtrHandle& aOnError, nsresult anError, DWORD anOSError); nsresult ReportSuccess( const nsMainThreadPtrHandle& aOnSuccess, const nsAString& aResourcePath); nsresult AddDirectoryToWatchList(WatchedResourceDescriptor* aDirectoryDescriptor); void AppendCallbacksToHashtables( const nsAString& aPath, const nsMainThreadPtrHandle& aOnChange, const nsMainThreadPtrHandle& aOnError); void RemoveCallbacksFromHashtables( const nsAString& aPath, const nsMainThreadPtrHandle& aOnChange, const nsMainThreadPtrHandle& aOnError); nsresult MakeResourcePath( WatchedResourceDescriptor* changedDescriptor, const nsAString& resourceName, nsAString& nativeResourcePath); }; /** * The watching thread logic. * * @return NS_OK if the watcher loop must be rescheduled, a failure code * if it must not. */ nsresult NativeFileWatcherIOTask::RunInternal() { // Contains the address of the |OVERLAPPED| structure passed // to ReadDirectoryChangesW (used to check for |HANDLE| closing). OVERLAPPED* overlappedStructure; // The number of bytes transferred by GetQueuedCompletionStatus // (used to check for |HANDLE| closing). DWORD transferredBytes = 0; // Will hold the |HANDLE| to the watched resource returned by GetQueuedCompletionStatus // which generated the change events. ULONG_PTR changedResourceHandle = 0; // Check for changes in the resource status by querying the |mIOCompletionPort| // (blocking). GetQueuedCompletionStatus is always called before the first call // to ReadDirectoryChangesW. This isn't a problem, since mIOCompletionPort is // already a valid |HANDLE| even though it doesn't have any associated notification // handles (through ReadDirectoryChangesW). if (!GetQueuedCompletionStatus(mIOCompletionPort, &transferredBytes, &changedResourceHandle, &overlappedStructure, INFINITE)) { // Ok, there was some error. DWORD errCode = GetLastError(); switch (errCode) { case ERROR_NOTIFY_ENUM_DIR: { // There were too many changes and the notification buffer has overflowed. // We dispatch the special value "*" and reschedule. FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Notification buffer has overflowed"); WatchedResourceDescriptor* changedRes = mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle); nsresult rv = DispatchChangeCallbacks(changedRes, NS_LITERAL_STRING("*")); if (NS_FAILED(rv)) { // We failed to dispatch the error callbacks. Something very // bad happened to the main thread, so we bail out from the watcher thread. FILEWATCHERLOG( "NativeFileWatcherIOTask::Run - Failed to dispatch change callbacks (%x).", rv); return rv; } return NS_OK; } case ERROR_ABANDONED_WAIT_0: case ERROR_INVALID_HANDLE: { // If we reach this point, mIOCompletionPort was probably closed // and we need to close this thread. This condition is identified // by catching the ERROR_INVALID_HANDLE error. FILEWATCHERLOG( "NativeFileWatcherIOTask::Run - The completion port was closed (%x).", errCode); return NS_ERROR_ABORT; } case ERROR_OPERATION_ABORTED: { // Some path was unwatched! That's not really an error, now it is safe // to free the memory for the resource and call GetQueuedCompletionStatus // again. FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Path unwatched (%x).", errCode); WatchedResourceDescriptor* toFree = mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle); if (toFree) { // Take care of removing the resource and freeing the memory mWatchedResourcesByHandle.Remove((HANDLE)changedResourceHandle); // This last call eventually frees the memory mWatchedResourcesByPath.Remove(toFree->mPath); } return NS_OK; } default: { // It should probably never get here, but it's better to be safe. FILEWATCHERLOG("NativeFileWatcherIOTask::Run - Unknown error (%x).", errCode); return NS_ERROR_FAILURE; } } } // When an |HANDLE| associated to the completion I/O port is gracefully // closed, GetQueuedCompletionStatus still may return a status update. Moreover, // this can also be triggered when watching files on a network folder and losing // the connection. // That's an edge case we need to take care of for consistency by checking // for (!transferredBytes && overlappedStructure). See http://xania.org/200807/iocp if (!transferredBytes && (overlappedStructure || (!overlappedStructure && !changedResourceHandle))) { // Note: if changedResourceHandle is nullptr as well, the wait on the Completion // I/O was interrupted by a call to PostQueuedCompletionStatus with 0 transferred // bytes and nullptr as |OVERLAPPED| and |HANDLE|. This is done to allow addPath // and removePath to work on this thread. return NS_OK; } // Check to see which resource is changedResourceHandle. WatchedResourceDescriptor* changedRes = mWatchedResourcesByHandle.Get((HANDLE)changedResourceHandle); MOZ_ASSERT(changedRes, "Could not find the changed resource in the list of watched ones."); // Parse the changes and notify the main thread. const unsigned char* rawNotificationBuffer = changedRes->mNotificationBuffer.get(); while (true) { FILE_NOTIFY_INFORMATION* notificationInfo = (FILE_NOTIFY_INFORMATION*)rawNotificationBuffer; // FileNameLength is in bytes, but we need FileName length // in characters, so divide it by sizeof(WCHAR). nsAutoString resourceName(notificationInfo->FileName, notificationInfo->FileNameLength / sizeof(WCHAR)); // Handle path normalisation using nsILocalFile. nsString resourcePath; nsresult rv = MakeResourcePath(changedRes, resourceName, resourcePath); if (NS_SUCCEEDED(rv)) { rv = DispatchChangeCallbacks(changedRes, resourcePath); if (NS_FAILED(rv)) { // Log that we failed to dispatch the change callbacks. FILEWATCHERLOG( "NativeFileWatcherIOTask::Run - Failed to dispatch change callbacks (%x).", rv); return rv; } } if (!notificationInfo->NextEntryOffset) { break; } rawNotificationBuffer += notificationInfo->NextEntryOffset; } // We need to keep watching for further changes. nsresult rv = AddDirectoryToWatchList(changedRes); if (NS_FAILED(rv)) { // We failed to watch the folder. if (rv == NS_ERROR_ABORT) { // Log that we also failed to dispatch the error callbacks. FILEWATCHERLOG( "NativeFileWatcherIOTask::Run - Failed to watch %s and" " to dispatch the related error callbacks", changedRes->mPath.get()); return rv; } } return NS_OK; } /** * Wraps the watcher logic and takes care of rescheduling * the watcher loop based on the return code of |RunInternal| * in order to help with code readability. * * @return NS_OK or a failure error code from |NS_DispatchToCurrentThread|. */ NS_IMETHODIMP NativeFileWatcherIOTask::Run() { MOZ_ASSERT(!NS_IsMainThread()); // We return immediately if |mShuttingDown| is true (see below for // details about the shutdown protocol being followed). if (mShuttingDown) { return NS_OK; } nsresult rv = RunInternal(); if (NS_FAILED(rv)) { // A critical error occurred in the watcher loop, don't reschedule. FILEWATCHERLOG( "NativeFileWatcherIOTask::Run - Stopping the watcher loop (error %S)", rv); // We log the error but return NS_OK instead: we don't want to // propagate an exception through XPCOM. return NS_OK; } // No error occurred, reschedule. return NS_DispatchToCurrentThread(this); } /** * Adds the resource to the watched list. This function is enqueued on the worker * thread by NativeFileWatcherService::AddPath. All the errors are reported to the main * thread using the error callback function mErrorCallback. * * @param pathToWatch * The path of the resource to watch for changes. * * @return NS_ERROR_FILE_NOT_FOUND if the path is invalid or does not exist. * Returns NS_ERROR_UNEXPECTED if OS |HANDLE|s are unexpectedly closed. * If the ReadDirectoryChangesW call fails, returns NS_ERROR_FAILURE, * otherwise NS_OK. */ nsresult NativeFileWatcherIOTask::AddPathRunnableMethod( PathRunnablesParametersWrapper* aWrappedParameters) { MOZ_ASSERT(!NS_IsMainThread()); nsAutoPtr wrappedParameters(aWrappedParameters); // We return immediately if |mShuttingDown| is true (see below for // details about the shutdown protocol being followed). if (mShuttingDown) { return NS_OK; } if (!wrappedParameters || !wrappedParameters->mChangeCallbackHandle) { FILEWATCHERLOG("NativeFileWatcherIOTask::AddPathRunnableMethod - Invalid arguments."); return NS_ERROR_NULL_POINTER; } // Is aPathToWatch already being watched? WatchedResourceDescriptor* watchedResource = mWatchedResourcesByPath.Get(wrappedParameters->mPath); if (watchedResource) { // Append it to the hash tables. AppendCallbacksToHashtables( watchedResource->mPath, wrappedParameters->mChangeCallbackHandle, wrappedParameters->mErrorCallbackHandle); return NS_OK; } // Retrieve a file handle to associate with the completion port. Makes // sure to request the appropriate rights (i.e. read files and list // files contained in a folder). Note: the nullptr security flag prevents // the |HANDLE| to be passed to child processes. HANDLE resHandle = CreateFileW(wrappedParameters->mPath.get(), FILE_LIST_DIRECTORY, // Access rights FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, // Share nullptr, // Security flags OPEN_EXISTING, // Returns an handle only if the resource exists FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, nullptr); // Template file (only used when creating files) if (resHandle == INVALID_HANDLE_VALUE) { DWORD dwError = GetLastError(); nsresult rv; if (dwError == ERROR_FILE_NOT_FOUND) { rv = NS_ERROR_FILE_NOT_FOUND; } else if (dwError == ERROR_ACCESS_DENIED) { rv = NS_ERROR_FILE_ACCESS_DENIED; } else { rv = NS_ERROR_FAILURE; } FILEWATCHERLOG( "NativeFileWatcherIOTask::AddPathRunnableMethod - CreateFileW failed (error %x) for %S.", dwError, wrappedParameters->mPath.get()); rv = ReportError(wrappedParameters->mErrorCallbackHandle, rv, dwError); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::AddPathRunnableMethod - " "Failed to dispatch the error callback (%x).", rv); return rv; } // Error has already been reported through mErrorCallback. return NS_OK; } // Initialise the resource descriptor. UniquePtr resourceDesc( new WatchedResourceDescriptor(wrappedParameters->mPath, resHandle)); // Associate the file with the previously opened completion port. if (!CreateIoCompletionPort(resourceDesc->mResourceHandle, mIOCompletionPort, (ULONG_PTR)resourceDesc->mResourceHandle.get(), 0)) { DWORD dwError = GetLastError(); FILEWATCHERLOG("NativeFileWatcherIOTask::AddPathRunnableMethod" " - CreateIoCompletionPort failed (error %x) for %S.", dwError, wrappedParameters->mPath.get()); // This could fail because passed parameters could be invalid |HANDLE|s // i.e. mIOCompletionPort was unexpectedly closed or failed. nsresult rv = ReportError(wrappedParameters->mErrorCallbackHandle, NS_ERROR_UNEXPECTED, dwError); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::AddPathRunnableMethod - " "Failed to dispatch the error callback (%x).", rv); return rv; } // Error has already been reported through mErrorCallback. return NS_OK; } // Append the callbacks to the hash tables. We do this now since // AddDirectoryToWatchList could use the error callback, but we // need to make sure to remove them if AddDirectoryToWatchList fails. AppendCallbacksToHashtables( wrappedParameters->mPath, wrappedParameters->mChangeCallbackHandle, wrappedParameters->mErrorCallbackHandle); // We finally watch the resource for changes. nsresult rv = AddDirectoryToWatchList(resourceDesc.get()); if (NS_SUCCEEDED(rv)) { // Add the resource pointer to both indexes. WatchedResourceDescriptor* resource = resourceDesc.release(); mWatchedResourcesByPath.Put(wrappedParameters->mPath, resource); mWatchedResourcesByHandle.Put(resHandle, resource); // Dispatch the success callback. nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::AddPathRunnableMethod - " "Failed to dispatch the success callback (%x).", rv); return rv; } return NS_OK; } // We failed to watch the folder. Remove the callbacks // from the hash tables. RemoveCallbacksFromHashtables( wrappedParameters->mPath, wrappedParameters->mChangeCallbackHandle, wrappedParameters->mErrorCallbackHandle); if (rv != NS_ERROR_ABORT) { // Just don't add the descriptor to the watch list. return NS_OK; } // We failed to dispatch the error callbacks as well. FILEWATCHERLOG( "NativeFileWatcherIOTask::AddPathRunnableMethod - Failed to watch %s and" " to dispatch the related error callbacks", resourceDesc->mPath.get()); return rv; } /** * Removes the path from the list of watched resources. Silently ignores the request * if the path was not being watched. * * Remove Protocol: * * 1. Find the resource to unwatch through the provided path. * 2. Remove the error and change callbacks from the list of callbacks * associated with the resource. * 3. Remove the error and change callbacks from the callback hash maps. * 4. If there are no more change callbacks for the resource, close * its file |HANDLE|. We do not free the buffer memory just yet, it's * still needed for the next call to GetQueuedCompletionStatus. That * memory will be freed in NativeFileWatcherIOTask::Run. * * @param aWrappedParameters * The structure containing the resource path, the error and change callback * handles. */ nsresult NativeFileWatcherIOTask::RemovePathRunnableMethod( PathRunnablesParametersWrapper* aWrappedParameters) { MOZ_ASSERT(!NS_IsMainThread()); nsAutoPtr wrappedParameters(aWrappedParameters); // We return immediately if |mShuttingDown| is true (see below for // details about the shutdown protocol being followed). if (mShuttingDown) { return NS_OK; } if (!wrappedParameters || !wrappedParameters->mChangeCallbackHandle) { return NS_ERROR_NULL_POINTER; } WatchedResourceDescriptor* toRemove = mWatchedResourcesByPath.Get(wrappedParameters->mPath); if (!toRemove) { // We are trying to remove a path which wasn't being watched. Silently ignore // and dispatch the success callback. nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::RemovePathRunnableMethod - " "Failed to dispatch the success callback (%x).", rv); return rv; } return NS_OK; } ChangeCallbackArray* changeCallbackArray = mChangeCallbacksTable.Get(toRemove->mPath); // This should always be valid. MOZ_ASSERT(changeCallbackArray); bool removed = changeCallbackArray->RemoveElement(wrappedParameters->mChangeCallbackHandle); if (!removed) { FILEWATCHERLOG( "NativeFileWatcherIOTask::RemovePathRunnableMethod - Unable to remove the change " "callback from the change callback hash map for %S.", wrappedParameters->mPath.get()); MOZ_CRASH(); } ErrorCallbackArray* errorCallbackArray = mErrorCallbacksTable.Get(toRemove->mPath); MOZ_ASSERT(errorCallbackArray); removed = errorCallbackArray->RemoveElement(wrappedParameters->mErrorCallbackHandle); if (!removed) { FILEWATCHERLOG( "NativeFileWatcherIOTask::RemovePathRunnableMethod - Unable to remove the error " "callback from the error callback hash map for %S.", wrappedParameters->mPath.get()); MOZ_CRASH(); } // If there are still callbacks left, keep the descriptor. // We don't check for error callbacks since there's no point in keeping // the descriptor if there are no change callbacks but some error callbacks. if (changeCallbackArray->Length()) { // Dispatch the success callback. nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::RemovePathRunnableMethod - " "Failed to dispatch the success callback (%x).", rv); return rv; } return NS_OK; } // In this runnable, we just cancel callbacks (see above) and I/O (see below). // Resources are freed by the worker thread when GetQueuedCompletionStatus // detects that a resource was removed from the watch list. // Since when closing |HANDLE|s relative to watched resources // GetQueuedCompletionStatus is notified one last time, it would end // up referring to deallocated memory if we were to free the memory here. // This happens because the worker IO is scheduled to watch the resources // again once we complete executing this function. // Enforce CloseHandle/CancelIO by disposing the AutoCloseHandle. We don't // remove the entry from mWatchedResourceBy* since the completion port might // still be using the notification buffer. Entry remove is performed when // handling ERROR_OPERATION_ABORTED in NativeFileWatcherIOTask::Run. toRemove->mResourceHandle.dispose(); // Dispatch the success callback. nsresult rv = ReportSuccess(wrappedParameters->mSuccessCallbackHandle, wrappedParameters->mPath); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::RemovePathRunnableMethod - " "Failed to dispatch the success callback (%x).", rv); return rv; } return NS_OK; } /** * Removes all the watched resources from the watch list and stops the * watcher thread. Frees all the used resources. */ nsresult NativeFileWatcherIOTask::DeactivateRunnableMethod() { MOZ_ASSERT(!NS_IsMainThread()); // Remind users to manually remove the watches before quitting. MOZ_ASSERT(!mWatchedResourcesByHandle.Count(), "Clients of the nsINativeFileWatcher must remove " "watches manually before quitting."); // Log any pending watch. for (auto it = mWatchedResourcesByHandle.Iter(); !it.Done(); it.Next()) { FILEWATCHERLOG("NativeFileWatcherIOTask::DeactivateRunnableMethod - " "%S is still being watched.", it.UserData()->mPath.get()); } // We return immediately if |mShuttingDown| is true (see below for // details about the shutdown protocol being followed). if (mShuttingDown) { // If this happens, we are in a strange situation. FILEWATCHERLOG( "NativeFileWatcherIOTask::DeactivateRunnableMethod - We are already shutting down."); MOZ_CRASH(); return NS_OK; } // Deactivate all the non-shutdown methods of this object. mShuttingDown = true; // Remove all the elements from the index. Memory will be freed by // calling Clear() on mWatchedResourcesByPath. mWatchedResourcesByHandle.Clear(); // Clear frees the memory associated with each element and clears the table. // Since we are using Scoped |HANDLE|s, they get automatically closed as well. mWatchedResourcesByPath.Clear(); // Now that all the descriptors are closed, release the callback hahstables. mChangeCallbacksTable.Clear(); mErrorCallbacksTable.Clear(); // Close the IO completion port, eventually making // the watcher thread exit from the watching loop. if (mIOCompletionPort) { if (!CloseHandle(mIOCompletionPort)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::DeactivateRunnableMethod - " "Failed to close the IO completion port HANDLE."); } } // Now we just need to reschedule a final call to Shutdown() back to the main thread. RefPtr shutdownRunnable = new NativeWatcherIOShutdownTask(); return NS_DispatchToMainThread(shutdownRunnable); } /** * Helper function to dispatch a change notification to all the registered callbacks. * @param aResourceDescriptor * The resource descriptor. * @param aChangedResource * The path of the changed resource. * @return NS_OK if all the callbacks are dispatched correctly, a |nsresult| error code * otherwise. */ nsresult NativeFileWatcherIOTask::DispatchChangeCallbacks( WatchedResourceDescriptor* aResourceDescriptor, const nsAString& aChangedResource) { MOZ_ASSERT(aResourceDescriptor); // Retrieve the change callbacks array. ChangeCallbackArray* changeCallbackArray = mChangeCallbacksTable.Get(aResourceDescriptor->mPath); // This should always be valid. MOZ_ASSERT(changeCallbackArray); for (size_t i = 0; i < changeCallbackArray->Length(); i++) { nsresult rv = ReportChange((*changeCallbackArray)[i], aChangedResource); if (NS_FAILED(rv)) { return rv; } } return NS_OK; } /** * Helper function to post a change runnable to the main thread. * * @param aOnChange * The change callback handle. * @param aChangedResource * The resource name to dispatch thorough the change callback. * * @return NS_OK if the callback is dispatched correctly. */ nsresult NativeFileWatcherIOTask::ReportChange( const nsMainThreadPtrHandle& aOnChange, const nsAString& aChangedResource) { RefPtr changeRunnable = new WatchedChangeEvent(aOnChange, aChangedResource); return NS_DispatchToMainThread(changeRunnable); } /** * Helper function to dispatch a error notification to all the registered callbacks. * @param aResourceDescriptor * The resource descriptor. * @param anError * The error to dispatch thorough the error callback. * @param anOSError * An OS specific error code to send with the callback. * @return NS_OK if all the callbacks are dispatched correctly, a |nsresult| error code * otherwise. */ nsresult NativeFileWatcherIOTask::DispatchErrorCallbacks( WatchedResourceDescriptor* aResourceDescriptor, nsresult anError, DWORD anOSError) { MOZ_ASSERT(aResourceDescriptor); // Retrieve the error callbacks array. ErrorCallbackArray* errorCallbackArray = mErrorCallbacksTable.Get(aResourceDescriptor->mPath); // This must be valid. MOZ_ASSERT(errorCallbackArray); for (size_t i = 0; i < errorCallbackArray->Length(); i++) { nsresult rv = ReportError((*errorCallbackArray)[i], anError, anOSError); if (NS_FAILED(rv)) { return rv; } } return NS_OK; } /** * Helper function to post an error runnable to the main thread. * * @param aOnError * The error callback handle. * @param anError * The error to dispatch thorough the error callback. * @param anOSError * An OS specific error code to send with the callback. * * @return NS_OK if the callback is dispatched correctly. */ nsresult NativeFileWatcherIOTask::ReportError( const nsMainThreadPtrHandle& aOnError, nsresult anError, DWORD anOSError) { RefPtr errorRunnable = new WatchedErrorEvent(aOnError, anError, anOSError); return NS_DispatchToMainThread(errorRunnable); } /** * Helper function to post a success runnable to the main thread. * * @param aOnSuccess * The success callback handle. * @param aResource * The resource name to dispatch thorough the success callback. * * @return NS_OK if the callback is dispatched correctly. */ nsresult NativeFileWatcherIOTask::ReportSuccess( const nsMainThreadPtrHandle& aOnSuccess, const nsAString& aResource) { RefPtr successRunnable = new WatchedSuccessEvent(aOnSuccess, aResource); return NS_DispatchToMainThread(successRunnable); } /** * Instructs the OS to report the changes concerning the directory of interest. * * @param aDirectoryDescriptor * A |WatchedResourceDescriptor| instance describing the directory to watch. * @param aDispatchErrorCode * If |ReadDirectoryChangesW| fails and dispatching an error callback to the * main thread fails as well, the error code is stored here. If the OS API call * does not fail, it gets set to NS_OK. * @return |true| if |ReadDirectoryChangesW| returned no error, |false| otherwise. */ nsresult NativeFileWatcherIOTask::AddDirectoryToWatchList( WatchedResourceDescriptor* aDirectoryDescriptor) { MOZ_ASSERT(!mShuttingDown); DWORD dwPlaceholder; // Tells the OS to watch out on mResourceHandle for the changes specified // with the FILE_NOTIFY_* flags. We monitor the creation, renaming and // deletion of a file (FILE_NOTIFY_CHANGE_FILE_NAME), changes to the last // modification time (FILE_NOTIFY_CHANGE_LAST_WRITE) and the creation and // deletion of a folder (FILE_NOTIFY_CHANGE_DIR_NAME). Moreover, when you // first call this function, the system allocates a buffer to store change // information for the watched directory. if (!ReadDirectoryChangesW(aDirectoryDescriptor->mResourceHandle, aDirectoryDescriptor->mNotificationBuffer.get(), NOTIFICATION_BUFFER_SIZE, true, // watch subtree (recurse) FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, &dwPlaceholder, &aDirectoryDescriptor->mOverlappedInfo, nullptr)) { // NOTE: GetLastError() could return ERROR_INVALID_PARAMETER if the buffer length // is greater than 64 KB and the application is monitoring a directory over the // network. The same error could be returned when trying to watch a file instead // of a directory. // It could return ERROR_NOACCESS if the buffer is not aligned on a DWORD boundary. DWORD dwError = GetLastError(); FILEWATCHERLOG( "NativeFileWatcherIOTask::AddDirectoryToWatchList " " - ReadDirectoryChangesW failed (error %x) for %S.", dwError, aDirectoryDescriptor->mPath.get()); nsresult rv = DispatchErrorCallbacks(aDirectoryDescriptor, NS_ERROR_FAILURE, dwError); if (NS_FAILED(rv)) { // That's really bad. We failed to watch the directory and failed to // dispatch the error callbacks. return NS_ERROR_ABORT; } // We failed to watch the directory, but we correctly dispatched the error callbacks. return NS_ERROR_FAILURE; } return NS_OK; } /** * Appends the change and error callbacks to their respective hash tables. * It also checks if the callbacks are already attached to them. * @param aPath * The watched directory path. * @param aOnChangeHandle * The callback to invoke when a change is detected. * @param aOnErrorHandle * The callback to invoke when an error is detected. */ void NativeFileWatcherIOTask::AppendCallbacksToHashtables( const nsAString& aPath, const nsMainThreadPtrHandle& aOnChangeHandle, const nsMainThreadPtrHandle& aOnErrorHandle) { // First check to see if we've got an entry already. ChangeCallbackArray* callbacksArray = mChangeCallbacksTable.Get(aPath); if (!callbacksArray) { // We don't have an entry. Create an array and put it into the hash table. callbacksArray = new ChangeCallbackArray(); mChangeCallbacksTable.Put(aPath, callbacksArray); } // We do have an entry for that path. Check to see if the callback is // already there. ChangeCallbackArray::index_type changeCallbackIndex = callbacksArray->IndexOf(aOnChangeHandle); // If the callback is not attached to the descriptor, append it. if (changeCallbackIndex == ChangeCallbackArray::NoIndex) { callbacksArray->AppendElement(aOnChangeHandle); } // Same thing for the error callback. ErrorCallbackArray* errorCallbacksArray = mErrorCallbacksTable.Get(aPath); if (!errorCallbacksArray) { // We don't have an entry. Create an array and put it into the hash table. errorCallbacksArray = new ErrorCallbackArray(); mErrorCallbacksTable.Put(aPath, errorCallbacksArray); } ErrorCallbackArray::index_type errorCallbackIndex = errorCallbacksArray->IndexOf(aOnErrorHandle); if (errorCallbackIndex == ErrorCallbackArray::NoIndex) { errorCallbacksArray->AppendElement(aOnErrorHandle); } } /** * Removes the change and error callbacks from their respective hash tables. * @param aPath * The watched directory path. * @param aOnChangeHandle * The change callback to remove. * @param aOnErrorHandle * The error callback to remove. */ void NativeFileWatcherIOTask::RemoveCallbacksFromHashtables( const nsAString& aPath, const nsMainThreadPtrHandle& aOnChangeHandle, const nsMainThreadPtrHandle& aOnErrorHandle) { // Find the change callback array for |aPath|. ChangeCallbackArray* callbacksArray = mChangeCallbacksTable.Get(aPath); if (callbacksArray) { // Remove the change callback. callbacksArray->RemoveElement(aOnChangeHandle); } // Find the error callback array for |aPath|. ErrorCallbackArray* errorCallbacksArray = mErrorCallbacksTable.Get(aPath); if (errorCallbacksArray) { // Remove the error callback. errorCallbacksArray->RemoveElement(aOnErrorHandle); } } /** * Creates a string representing the native path for the changed resource. * It appends the resource name to the path of the changed descriptor by * using nsILocalFile. * @param changedDescriptor * The descriptor of the watched resource. * @param resourceName * The resource which triggered the change. * @param nativeResourcePath * The full path to the changed resource. * @return NS_OK if nsILocalFile succeeded in building the path. */ nsresult NativeFileWatcherIOTask::MakeResourcePath( WatchedResourceDescriptor* changedDescriptor, const nsAString& resourceName, nsAString& nativeResourcePath) { nsCOMPtr localPath(do_CreateInstance("@mozilla.org/file/local;1")); if (!localPath) { FILEWATCHERLOG( "NativeFileWatcherIOTask::MakeResourcePath - Failed to create a nsILocalFile instance."); return NS_ERROR_FAILURE; } nsresult rv = localPath->InitWithPath(changedDescriptor->mPath); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::MakeResourcePath - Failed to init nsILocalFile with %S (%x).", changedDescriptor->mPath.get(), rv); return rv; } rv = localPath->AppendRelativePath(resourceName); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::MakeResourcePath - Failed to append to %S (%x).", changedDescriptor->mPath.get(), rv); return rv; } rv = localPath->GetPath(nativeResourcePath); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::MakeResourcePath - Failed to get native path from nsILocalFile (%x).", rv); return rv; } return NS_OK; } } // namespace // The NativeFileWatcherService component NS_IMPL_ISUPPORTS(NativeFileWatcherService, nsINativeFileWatcherService, nsIObserver); NativeFileWatcherService::NativeFileWatcherService() { } NativeFileWatcherService::~NativeFileWatcherService() { } /** * Sets the required resources and starts the watching IO thread. * * @return NS_OK if there was no error with thread creation and execution. */ nsresult NativeFileWatcherService::Init() { // Creates an IO completion port and allows at most 2 thread to access it concurrently. AutoCloseHandle completionPort( CreateIoCompletionPort(INVALID_HANDLE_VALUE, // FileHandle nullptr, // ExistingCompletionPort 0, // CompletionKey 2)); // NumberOfConcurrentThreads if (!completionPort) { return NS_ERROR_FAILURE; } // Add an observer for the shutdown. nsCOMPtr observerService = mozilla::services::GetObserverService(); if (!observerService) { return NS_ERROR_FAILURE; } observerService->AddObserver(this, "xpcom-shutdown-threads", false); // Start the IO worker thread. mWorkerIORunnable = new NativeFileWatcherIOTask(completionPort); nsresult rv = NS_NewThread(getter_AddRefs(mIOThread), mWorkerIORunnable); if (NS_FAILED(rv)) { FILEWATCHERLOG( "NativeFileWatcherIOTask::Init - Unable to create and dispatch the worker thread (%x).", rv); return rv; } // Set the name for the worker thread. NS_SetThreadName(mIOThread, "FileWatcher IO"); mIOCompletionPort = completionPort.forget(); return NS_OK; } /** * Watches a path for changes: monitors the creations, name changes and * content changes to the files contained in the watched path. * * @param aPathToWatch * The path of the resource to watch for changes. * @param aOnChange * The callback to invoke when a change is detected. * @param aOnError * The optional callback to invoke when there's an error. * @param aOnSuccess * The optional callback to invoke when the file watcher starts * watching the resource for changes. * * @return NS_OK or NS_ERROR_NOT_INITIALIZED if the instance was not initialized. * Other errors are reported by the error callback function. */ NS_IMETHODIMP NativeFileWatcherService::AddPath(const nsAString& aPathToWatch, nsINativeFileWatcherCallback* aOnChange, nsINativeFileWatcherErrorCallback* aOnError, nsINativeFileWatcherSuccessCallback* aOnSuccess) { // Make sure the instance was initialized. if (!mIOThread) { return NS_ERROR_NOT_INITIALIZED; } // Be sure a valid change callback was passed. if (!aOnChange) { return NS_ERROR_NULL_POINTER; } nsMainThreadPtrHandle changeCallbackHandle( new nsMainThreadPtrHolder(aOnChange)); nsMainThreadPtrHandle errorCallbackHandle( new nsMainThreadPtrHolder(aOnError)); nsMainThreadPtrHandle successCallbackHandle( new nsMainThreadPtrHolder(aOnSuccess)); // Wrap the path and the callbacks in order to pass them using NewRunnableMethod. UniquePtr wrappedCallbacks( new PathRunnablesParametersWrapper( aPathToWatch, changeCallbackHandle, errorCallbackHandle, successCallbackHandle)); // Since this function does a bit of I/O stuff , run it in the IO thread. nsresult rv = mIOThread->Dispatch( NewRunnableMethod( static_cast(mWorkerIORunnable.get()), &NativeFileWatcherIOTask::AddPathRunnableMethod, wrappedCallbacks.get()), nsIEventTarget::DISPATCH_NORMAL); if (NS_FAILED(rv)) { return rv; } // Since the dispatch succeeded, we let the runnable own the pointer. wrappedCallbacks.release(); WakeUpWorkerThread(); return NS_OK; } /** * Removes the path from the list of watched resources. Silently ignores the request * if the path was not being watched or the callbacks were not registered. * * @param aPathToRemove * The path of the resource to remove from the watch list. * @param aOnChange * The callback to invoke when a change is detected. * @param aOnError * The optionally registered callback invoked when there's an error. * @param aOnSuccess * The optional callback to invoke when the file watcher stops * watching the resource for changes. * * @return NS_OK or NS_ERROR_NOT_INITIALIZED if the instance was not initialized. * Other errors are reported by the error callback function. */ NS_IMETHODIMP NativeFileWatcherService::RemovePath(const nsAString& aPathToRemove, nsINativeFileWatcherCallback* aOnChange, nsINativeFileWatcherErrorCallback* aOnError, nsINativeFileWatcherSuccessCallback* aOnSuccess) { // Make sure the instance was initialized. if (!mIOThread) { return NS_ERROR_NOT_INITIALIZED; } // Be sure a valid change callback was passed. if (!aOnChange) { return NS_ERROR_NULL_POINTER; } nsMainThreadPtrHandle changeCallbackHandle( new nsMainThreadPtrHolder(aOnChange)); nsMainThreadPtrHandle errorCallbackHandle( new nsMainThreadPtrHolder(aOnError)); nsMainThreadPtrHandle successCallbackHandle( new nsMainThreadPtrHolder(aOnSuccess)); // Wrap the path and the callbacks in order to pass them using NewRunnableMethod. UniquePtr wrappedCallbacks( new PathRunnablesParametersWrapper( aPathToRemove, changeCallbackHandle, errorCallbackHandle, successCallbackHandle)); // Since this function does a bit of I/O stuff, run it in the IO thread. nsresult rv = mIOThread->Dispatch( NewRunnableMethod( static_cast(mWorkerIORunnable.get()), &NativeFileWatcherIOTask::RemovePathRunnableMethod, wrappedCallbacks.get()), nsIEventTarget::DISPATCH_NORMAL); if (NS_FAILED(rv)) { return rv; } // Since the dispatch succeeded, we let the runnable own the pointer. wrappedCallbacks.release(); WakeUpWorkerThread(); return NS_OK; } /** * Removes all the watched resources from the watch list and stops the * watcher thread. Frees all the used resources. * * To avoid race conditions, we need a Shutdown Protocol: * * 1. [MainThread] * When the "xpcom-shutdown-threads" event is detected, Uninit() gets called. * 2. [MainThread] * Uninit sends DeactivateRunnableMethod() to the WorkerThread. * 3. [WorkerThread] * DeactivateRunnableMethod makes it clear to other methods that shutdown is * in progress, stops the IO completion port wait and schedules the rest of the * deactivation for after every currently pending method call is complete. */ nsresult NativeFileWatcherService::Uninit() { // Make sure the instance was initialized (and not de-initialized yet). if (!mIOThread) { return NS_OK; } // We need to be sure that there will be no calls to 'mIOThread' once we have entered // 'Uninit()', even if we exit due to an error. nsCOMPtr ioThread; ioThread.swap(mIOThread); // Since this function does a bit of I/O stuff (close file handle), run it // in the IO thread. nsresult rv = ioThread->Dispatch( NewRunnableMethod( static_cast(mWorkerIORunnable.get()), &NativeFileWatcherIOTask::DeactivateRunnableMethod), nsIEventTarget::DISPATCH_NORMAL); if (NS_FAILED(rv)) { return rv; } WakeUpWorkerThread(); return NS_OK; } /** * Tells |NativeFileWatcherIOTask| to quit and to reschedule itself in order to * execute the other runnables enqueued in the worker tread. * This works by posting a bogus event to the blocking |GetQueuedCompletionStatus| * call in |NativeFileWatcherIOTask::Run()|. */ void NativeFileWatcherService::WakeUpWorkerThread() { // The last 3 parameters represent the number of transferred bytes, the changed // resource |HANDLE| and the address of the |OVERLAPPED| structure passed to // GetQueuedCompletionStatus: we set them to nullptr so that we can recognize // that we requested an interruption from the Worker thread. PostQueuedCompletionStatus(mIOCompletionPort, 0, 0, nullptr); } /** * This method is used to catch the "xpcom-shutdown-threads" event in order * to shutdown this service when closing the application. */ NS_IMETHODIMP NativeFileWatcherService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { MOZ_ASSERT(NS_IsMainThread()); if (!strcmp("xpcom-shutdown-threads", aTopic)) { nsresult rv = Uninit(); MOZ_ASSERT(NS_SUCCEEDED(rv)); return NS_OK; } MOZ_ASSERT(false, "NativeFileWatcherService got an unexpected topic!"); return NS_ERROR_UNEXPECTED; } } // namespace mozilla