diff options
Diffstat (limited to 'toolkit/components/filewatcher')
21 files changed, 2561 insertions, 0 deletions
diff --git a/toolkit/components/filewatcher/NativeFileWatcherNotSupported.h b/toolkit/components/filewatcher/NativeFileWatcherNotSupported.h new file mode 100644 index 000000000..6b1aa97ba --- /dev/null +++ b/toolkit/components/filewatcher/NativeFileWatcherNotSupported.h @@ -0,0 +1,52 @@ +/* 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/. */ + +#ifndef mozilla_nativefilewatcher_h__ +#define mozilla_nativefilewatcher_h__ + +#include "nsINativeFileWatcher.h" + +namespace mozilla { + +class NativeFileWatcherService final : public nsINativeFileWatcherService +{ +public: + NS_DECL_ISUPPORTS + + NativeFileWatcherService() + { + }; + + nsresult Init() + { + return NS_OK; + }; + + NS_IMETHOD AddPath(const nsAString& aPathToWatch, + nsINativeFileWatcherCallback* aOnChange, + nsINativeFileWatcherErrorCallback* aOnError, + nsINativeFileWatcherSuccessCallback* aOnSuccess) override + { + return NS_ERROR_NOT_IMPLEMENTED; + }; + + NS_IMETHOD RemovePath(const nsAString& aPathToRemove, + nsINativeFileWatcherCallback* aOnChange, + nsINativeFileWatcherErrorCallback* aOnError, + nsINativeFileWatcherSuccessCallback* aOnSuccess) override + { + return NS_ERROR_NOT_IMPLEMENTED; + }; + +private: + ~NativeFileWatcherService() { }; + NativeFileWatcherService(const NativeFileWatcherService& other) = delete; + void operator=(const NativeFileWatcherService& other) = delete; +}; + +NS_IMPL_ISUPPORTS(NativeFileWatcherService, nsINativeFileWatcherService); + +} // namespace mozilla + +#endif // mozilla_nativefilewatcher_h__ diff --git a/toolkit/components/filewatcher/NativeFileWatcherWin.cpp b/toolkit/components/filewatcher/NativeFileWatcherWin.cpp new file mode 100644 index 000000000..3ff69728a --- /dev/null +++ b/toolkit/components/filewatcher/NativeFileWatcherWin.cpp @@ -0,0 +1,1494 @@ +/* -*- 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<nsINativeFileWatcherErrorCallback>& 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<nsINativeFileWatcherErrorCallback> 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<nsINativeFileWatcherSuccessCallback>& 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<nsINativeFileWatcherSuccessCallback> 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<nsINativeFileWatcherCallback>& 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<nsINativeFileWatcherCallback> 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<AutoCloseHandleTraits> AutoCloseHandle; + +// Define these callback array types to make the code easier to read. +typedef nsTArray<nsMainThreadPtrHandle<nsINativeFileWatcherCallback>> ChangeCallbackArray; +typedef nsTArray<nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>> 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<FILE_NOTIFY_INFORMATION> cannot be used as the structure + // contains a variable length field (FileName). + UniquePtr<unsigned char> 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<nsINativeFileWatcherCallback> mChangeCallbackHandle; + nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> mErrorCallbackHandle; + nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> mSuccessCallbackHandle; + + PathRunnablesParametersWrapper( + const nsAString& aPath, + const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange, + const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError, + const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>& 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<nsIThread> 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<nsStringHashKey, WatchedResourceDescriptor> mWatchedResourcesByPath; + nsDataHashtable<nsVoidPtrHashKey, WatchedResourceDescriptor*> 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<callbacks>. + nsClassHashtable<nsStringHashKey, ChangeCallbackArray> mChangeCallbacksTable; + nsClassHashtable<nsStringHashKey, ErrorCallbackArray> 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<nsINativeFileWatcherCallback>& aOnChange, + const nsAString& aChangedResource); + + nsresult DispatchErrorCallbacks(WatchedResourceDescriptor* aResourceDescriptor, + nsresult anError, DWORD anOSError); + + nsresult ReportError( + const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError, + nsresult anError, DWORD anOSError); + + nsresult ReportSuccess( + const nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback>& aOnSuccess, + const nsAString& aResourcePath); + + nsresult AddDirectoryToWatchList(WatchedResourceDescriptor* aDirectoryDescriptor); + + void AppendCallbacksToHashtables( + const nsAString& aPath, + const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange, + const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& aOnError); + + void RemoveCallbacksFromHashtables( + const nsAString& aPath, + const nsMainThreadPtrHandle<nsINativeFileWatcherCallback>& aOnChange, + const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& 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<PathRunnablesParametersWrapper> 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<WatchedResourceDescriptor> 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<PathRunnablesParametersWrapper> 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<NativeWatcherIOShutdownTask> 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<nsINativeFileWatcherCallback>& aOnChange, + const nsAString& aChangedResource) +{ + RefPtr<WatchedChangeEvent> 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<nsINativeFileWatcherErrorCallback>& aOnError, + nsresult anError, DWORD anOSError) +{ + RefPtr<WatchedErrorEvent> 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<nsINativeFileWatcherSuccessCallback>& aOnSuccess, + const nsAString& aResource) +{ + RefPtr<WatchedSuccessEvent> 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<nsINativeFileWatcherCallback>& aOnChangeHandle, + const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& 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<nsINativeFileWatcherCallback>& aOnChangeHandle, + const nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback>& 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<nsILocalFile> + 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<nsIObserverService> 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<nsINativeFileWatcherCallback> changeCallbackHandle( + new nsMainThreadPtrHolder<nsINativeFileWatcherCallback>(aOnChange)); + + nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> errorCallbackHandle( + new nsMainThreadPtrHolder<nsINativeFileWatcherErrorCallback>(aOnError)); + + nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> successCallbackHandle( + new nsMainThreadPtrHolder<nsINativeFileWatcherSuccessCallback>(aOnSuccess)); + + // Wrap the path and the callbacks in order to pass them using NewRunnableMethod. + UniquePtr<PathRunnablesParametersWrapper> 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<PathRunnablesParametersWrapper*>( + static_cast<NativeFileWatcherIOTask*>(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<nsINativeFileWatcherCallback> changeCallbackHandle( + new nsMainThreadPtrHolder<nsINativeFileWatcherCallback>(aOnChange)); + + nsMainThreadPtrHandle<nsINativeFileWatcherErrorCallback> errorCallbackHandle( + new nsMainThreadPtrHolder<nsINativeFileWatcherErrorCallback>(aOnError)); + + nsMainThreadPtrHandle<nsINativeFileWatcherSuccessCallback> successCallbackHandle( + new nsMainThreadPtrHolder<nsINativeFileWatcherSuccessCallback>(aOnSuccess)); + + // Wrap the path and the callbacks in order to pass them using NewRunnableMethod. + UniquePtr<PathRunnablesParametersWrapper> 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<PathRunnablesParametersWrapper*>( + static_cast<NativeFileWatcherIOTask*>(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<nsIThread> 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<NativeFileWatcherIOTask*>(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 diff --git a/toolkit/components/filewatcher/NativeFileWatcherWin.h b/toolkit/components/filewatcher/NativeFileWatcherWin.h new file mode 100644 index 000000000..37dd97f84 --- /dev/null +++ b/toolkit/components/filewatcher/NativeFileWatcherWin.h @@ -0,0 +1,50 @@ +/* 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/. */ + +#ifndef mozilla_nativefilewatcher_h__ +#define mozilla_nativefilewatcher_h__ + +#include "nsINativeFileWatcher.h" +#include "nsIObserver.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsThreadUtils.h" + +// We need to include this header here for HANDLE definition. +#include <windows.h> + +namespace mozilla { + +class NativeFileWatcherService final : public nsINativeFileWatcherService, + public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSINATIVEFILEWATCHERSERVICE + NS_DECL_NSIOBSERVER + + NativeFileWatcherService(); + + nsresult Init(); + +private: + // The |HANDLE| to the I/O Completion Port, owned by the main thread. + HANDLE mIOCompletionPort; + nsCOMPtr<nsIThread> mIOThread; + + // The instance of the runnable dealing with the I/O. + nsCOMPtr<nsIRunnable> mWorkerIORunnable; + + nsresult Uninit(); + void WakeUpWorkerThread(); + + // Make the dtor private to make this object only deleted via its ::Release() method. + ~NativeFileWatcherService(); + NativeFileWatcherService(const NativeFileWatcherService& other) = delete; + void operator=(const NativeFileWatcherService& other) = delete; +}; + +} // namespace mozilla + +#endif // mozilla_nativefilewatcher_h__ diff --git a/toolkit/components/filewatcher/moz.build b/toolkit/components/filewatcher/moz.build new file mode 100644 index 000000000..5ce94b5d2 --- /dev/null +++ b/toolkit/components/filewatcher/moz.build @@ -0,0 +1,23 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini'] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + EXPORTS += ['NativeFileWatcherWin.h'] + UNIFIED_SOURCES += [ + 'NativeFileWatcherWin.cpp', + ] +else: + EXPORTS += ['NativeFileWatcherNotSupported.h'] + +XPIDL_MODULE = 'toolkit_filewatcher' + +XPIDL_SOURCES += [ + 'nsINativeFileWatcher.idl', +] + +FINAL_LIBRARY = 'xul' diff --git a/toolkit/components/filewatcher/nsINativeFileWatcher.idl b/toolkit/components/filewatcher/nsINativeFileWatcher.idl new file mode 100644 index 000000000..afbe684c4 --- /dev/null +++ b/toolkit/components/filewatcher/nsINativeFileWatcher.idl @@ -0,0 +1,111 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=40: */ +/* 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/. */ + +#include "nsISupports.idl" + +/** + * The interface for the callback invoked when there is an error. + */ +[scriptable, function, uuid(5DAEDDC3-FC94-4880-8A4F-26D910B92662)] +interface nsINativeFileWatcherErrorCallback: nsISupports +{ + /** + * @param xpcomError The XPCOM error code. + * @param osError The native OS error (errno under Unix, GetLastError under Windows). + */ + void complete(in nsresult xpcomError, in long osError); +}; + +/** + * The interface for the callback invoked when a change on a watched + * resource is detected. + */ +[scriptable, function, uuid(FE4D86C9-243F-4195-B544-AECE3DF4B86A)] +interface nsINativeFileWatcherCallback: nsISupports +{ + /** + * @param resourcePath + * The path of the changed resource. If there were too many changes, + * the string "*" is passed. + * @param flags Reserved for future uses, not currently used. + */ + void changed(in AString resourcePath, in int32_t flags); +}; + +/** + * The interface for the callback invoked when a file watcher operation + * successfully completes. + */ +[scriptable, function, uuid(C3D7F542-681B-4ABD-9D65-9D799B29A42B)] +interface nsINativeFileWatcherSuccessCallback: nsISupports +{ + /** + * @param resourcePath + * The path of the resource for which the operation completes. + */ + void complete(in AString resourcePath); +}; + +/** + * A service providing native implementations of path changes notification. + */ +[scriptable, builtinclass, uuid(B3A4E8D8-7DC8-47DB-A8B4-83736D7AC1AA)] +interface nsINativeFileWatcherService: nsISupports +{ + /** + * Watches the passed path for changes. If it's a directory, every file + * it contains is watched. Recursively watches subdirectories. If the + * resource is already being watched, does nothing. If the passed path + * is a file, the behaviour is not specified. + * + * @param pathToWatch The path to watch for changes. + * @param onChange + * The callback invoked whenever a change on a watched + * resource is detected. + * @param onError + * The optional callback invoked whenever an error occurs. + * @param onSuccess + * The optional callback invoked when the file watcher starts + * watching the resource for changes. + */ + void addPath(in AString pathToWatch, + in nsINativeFileWatcherCallback onChange, + [optional] in nsINativeFileWatcherErrorCallback onError, + [optional] in nsINativeFileWatcherSuccessCallback onSuccess); + + /** + * Removes the provided path from the watched resources. If the path + * was not being watched or the callbacks were not registered, silently + * ignores the request. + * Please note that the file watcher only considers the onChange callbacks + * when deciding to close a watch on a resource. If there are no more onChange + * callbacks associated to the watch, it gets closed (even though it still has + * some error callbacks associated). + * + * @param pathToUnwatch The path to un-watch. + * @param onChange + * The registered callback invoked whenever a change on a watched + * resource is detected. + * @param onError + * The optionally registered callback invoked whenever an error + * occurs. + * @param onSuccess + * The optional callback invoked when the file watcher stops + * watching the resource for changes. + */ + void removePath(in AString pathToUnwatch, + in nsINativeFileWatcherCallback onChange, + [optional] in nsINativeFileWatcherErrorCallback onError, + [optional] in nsINativeFileWatcherSuccessCallback onSuccess); +}; + + +%{ C++ + +#define NATIVE_FILEWATCHER_SERVICE_CID {0x6F488507, 0x469D, 0x4350, {0xA6, 0x8D, 0x99, 0xC8, 0x7, 0xBE, 0xA, 0x78}} +#define NATIVE_FILEWATCHER_SERVICE_CONTRACTID "@mozilla.org/toolkit/filewatcher/native-file-watcher;1" + +%} diff --git a/toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js b/toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/filewatcher/tests/xpcshell/head.js b/toolkit/components/filewatcher/tests/xpcshell/head.js new file mode 100644 index 000000000..73f8ac4f5 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/head.js @@ -0,0 +1,29 @@ +/* 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/. */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); + +function makeWatcher() { + let watcher = + Cc['@mozilla.org/toolkit/filewatcher/native-file-watcher;1'] + .getService(Ci.nsINativeFileWatcherService); + return watcher; +} + +function promiseAddPath(watcher, resource, onChange=null, onError=null) { + return new Promise(resolve => + watcher.addPath(resource, onChange, onError, resolve) + ); +} + +function promiseRemovePath(watcher, resource, onChange=null, onError=null) { + return new Promise(resolve => + watcher.removePath(resource, onChange, onError, resolve) + ); +} diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_arguments.js b/toolkit/components/filewatcher/tests/xpcshell/test_arguments.js new file mode 100644 index 000000000..7e62b1cb6 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_arguments.js @@ -0,0 +1,79 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Test for addPath usage with null arguments. + */ +add_task(function* test_null_args_addPath() { + + let watcher = makeWatcher(); + let testPath = 'someInvalidPath'; + + // Define a dummy callback function. In this test no callback is + // expected to be called. + let dummyFunc = function(changed) { + do_throw("Not expected in this test."); + }; + + // Check for error when passing a null first argument + try { + watcher.addPath(testPath, null, dummyFunc); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NULL_POINTER) + throw ex; + do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected."); + } + + // Check for error when passing both null arguments + try { + watcher.addPath(testPath, null, null); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NULL_POINTER) + throw ex; + do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected."); + } +}); + +/** + * Test for removePath usage with null arguments. + */ +add_task(function* test_null_args_removePath() { + + let watcher = makeWatcher(); + let testPath = 'someInvalidPath'; + + // Define a dummy callback function. In this test no callback is + // expected to be called. + let dummyFunc = function(changed) { + do_throw("Not expected in this test."); + }; + + // Check for error when passing a null first argument + try { + watcher.removePath(testPath, null, dummyFunc); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NULL_POINTER) + throw ex; + do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected."); + } + + // Check for error when passing both null arguments + try { + watcher.removePath(testPath, null, null); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NULL_POINTER) + throw ex; + do_print("Initialisation thrown NS_ERROR_NULL_POINTER as expected."); + } +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.js b/toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.js new file mode 100644 index 000000000..e5ceb33e5 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_no_error_callback.js @@ -0,0 +1,69 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Test the component behaves correctly when no error callback is + * provided and an error occurs. + */ +add_task(function* test_error_with_no_error_callback() { + + let watcher = makeWatcher(); + let testPath = 'someInvalidPath'; + + // Define a dummy callback function. In this test no callback is + // expected to be called. + let dummyFunc = function(changed) { + do_throw("Not expected in this test."); + }; + + // We don't pass an error callback and try to watch an invalid + // path. + watcher.addPath(testPath, dummyFunc); +}); + +/** + * Test the component behaves correctly when no error callback is + * provided (no error should occur). + */ +add_task(function* test_watch_single_path_file_creation_no_error_cb() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + let tempFileName = "test_filecreation.tmp"; + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Watch the profile directory but do not pass an error callback. + yield promiseAddPath(watcher, watchedDir, deferred.resolve); + + // Create a file within the watched directory. + let tmpFilePath = OS.Path.join(watchedDir, tempFileName); + yield OS.File.writeAtomic(tmpFilePath, "some data"); + + // Wait until the watcher informs us that the file was created. + let changed = yield deferred.promise; + do_check_eq(changed, tmpFilePath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' to unregister). + watcher.removePath(watchedDir, deferred.resolve); + + // Remove the test directory and all of its content. + yield OS.File.removeDir(watchedDir); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.js b/toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.js new file mode 100644 index 000000000..1375584a3 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_remove_non_watched.js @@ -0,0 +1,39 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Test removing non watched path + */ +add_task(function* test_remove_not_watched() { + let nonExistingDir = + OS.Path.join(OS.Constants.Path.profileDir, "absolutelyNotExisting"); + + // Instantiate the native watcher. + let watcher = makeWatcher(); + + // Try to un-watch a path which wasn't being watched. + watcher.removePath( + nonExistingDir, + function(changed) { + do_throw("No change is expected in this test."); + }, + function(xpcomError, osError) { + // When removing a resource which wasn't being watched, it should silently + // ignore the request. + do_throw("Unexpected exception: " + + xpcomError + " (XPCOM) " + + osError + " (OS Error)"); + } + ); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.js b/toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.js new file mode 100644 index 000000000..482ba6b8b --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_shared_callback.js @@ -0,0 +1,62 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Test the watcher correctly handles two watches sharing the same + * change callback. + */ +add_task(function* test_watch_with_shared_callback() { + + // Create and watch two sub-directories of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDirs = + [ + OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"), + OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground2") + ]; + + yield OS.File.makeDir(watchedDirs[0]); + yield OS.File.makeDir(watchedDirs[1]); + + let tempFileName = "test_filecreation.tmp"; + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Watch both directories using the same callbacks. + yield promiseAddPath(watcher, watchedDirs[0], deferred.resolve, deferred.reject); + yield promiseAddPath(watcher, watchedDirs[1], deferred.resolve, deferred.reject); + + // Remove the watch for the first directory, but keep watching + // for changes in the second: we need to make sure the callback + // survives the removal of the first watch. + watcher.removePath(watchedDirs[0], deferred.resolve, deferred.reject); + + // Create a file within the watched directory. + let tmpFilePath = OS.Path.join(watchedDirs[1], tempFileName); + yield OS.File.writeAtomic(tmpFilePath, "some data"); + + // Wait until the watcher informs us that the file was created. + let changed = yield deferred.promise; + do_check_eq(changed, tmpFilePath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + watcher.removePath(watchedDirs[1], deferred.resolve, deferred.reject); + + // Remove the test directories and all of their content. + yield OS.File.removeDir(watchedDirs[0]); + yield OS.File.removeDir(watchedDirs[1]); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.js new file mode 100644 index 000000000..a434ec751 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_creation_single.js @@ -0,0 +1,49 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files + do_get_profile(); + + // Start executing the tests + run_next_test(); +} + +/** + * Tests that the watcher correctly notifies of a directory creation when watching + * a single path. + */ +add_task(function* test_watch_single_path_directory_creation() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + let tmpDirPath = OS.Path.join(watchedDir, "testdir"); + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching. + yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Once ready, create a directory within the watched directory. + yield OS.File.makeDir(tmpDirPath); + + // Wait until the watcher informs us that the file has changed. + let changed = yield deferred.promise; + do_check_eq(changed, tmpDirPath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Clean up the test directory. + yield OS.File.removeDir(watchedDir); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js new file mode 100644 index 000000000..2c74a9361 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_directory_deletion_single.js @@ -0,0 +1,46 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Tests that the watcher correctly notifies of a directory deletion when watching + * a single path. + */ +add_task(function* test_watch_single_path_directory_deletion() { + + let watchedDir = OS.Constants.Path.profileDir; + let tempDirName = "test"; + let tmpDirPath = OS.Path.join(watchedDir, tempDirName); + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Create a directory within the watched directory. + yield OS.File.makeDir(tmpDirPath); + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching it. + yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Remove the directory. + OS.File.removeDir(tmpDirPath); + + // Wait until the watcher informs us that the file has changed. + let changed = yield deferred.promise; + do_check_eq(changed, tmpDirPath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js new file mode 100644 index 000000000..9f87793f4 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_creation_single.js @@ -0,0 +1,51 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Test the watcher correctly notifies of a file creation when watching + * a single path. + */ +add_task(function* test_watch_single_path_file_creation() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + let tempFileName = "test_filecreation.tmp"; + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + let tmpFilePath = OS.Path.join(watchedDir, tempFileName); + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching. + yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // create the file within the watched directory. + yield OS.File.writeAtomic(tmpFilePath, "some data"); + + // Wait until the watcher informs us that the file was created. + let changed = yield deferred.promise; + do_check_eq(changed, tmpFilePath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Remove the test directory and all of its content. + yield OS.File.removeDir(watchedDir); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js new file mode 100644 index 000000000..97d2d61bc --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_deletion_single.js @@ -0,0 +1,54 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} +/** + * Test the watcher correctly notifies of a file deletion when watching + * a single path. + */ +add_task(function* test_watch_single_path_file_deletion() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + let tempFileName = "test_filedeletion.tmp"; + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Create a file within the directory to be watched. We do this + // before watching the directory so we do not get the creation notification. + let tmpFilePath = OS.Path.join(watchedDir, tempFileName); + yield OS.File.writeAtomic(tmpFilePath, "some data"); + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching it. + yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Remove the file we created (should trigger a notification). + do_print('Removing ' + tmpFilePath); + yield OS.File.remove(tmpFilePath); + + // Wait until the watcher informs us that the file was deleted. + let changed = yield deferred.promise; + do_check_eq(changed, tmpFilePath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Remove the test directory and all of its content. + yield OS.File.removeDir(watchedDir); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js new file mode 100644 index 000000000..ba25fdff6 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_file_modification_single.js @@ -0,0 +1,54 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files + do_get_profile(); + + // Start executing the tests + run_next_test(); +} + +/** + * Tests that the watcher correctly notifies of a file modification when watching + * a single path. + */ +add_task(function* test_watch_single_path_file_modification() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + let tempFileName = "test_filemodification.tmp"; + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Create a file within the directory to be watched. We do this + // before watching the directory so we do not get the creation notification. + let tmpFilePath = OS.Path.join(watchedDir, tempFileName); + yield OS.File.writeAtomic(tmpFilePath, "some data"); + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching it. + yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Once ready, modify the file to trigger the notification. + yield OS.File.writeAtomic(tmpFilePath, "some new data"); + + // Wait until the watcher informs us that the file has changed. + let changed = yield deferred.promise; + do_check_eq(changed, tmpFilePath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Remove the test directory and all of its content. + yield OS.File.removeDir(watchedDir); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.js new file mode 100644 index 000000000..c236c6e1d --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_many_changes.js @@ -0,0 +1,73 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files + do_get_profile(); + + // Start executing the tests + run_next_test(); +} + +/** + * Test that we correctly handle watching directories when hundreds of files + * change simultaneously. + */ +add_task(function* test_fill_notification_buffer() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + // The number of files to create. + let numberOfFiles = 100; + let fileNameBase = "testFile"; + + // This will be used to keep track of the number of changes within the watched + // directory. + let detectedChanges = 0; + + // We expect at least the following notifications for each file: + // - File creation + // - File deletion + let expectedChanges = numberOfFiles * 2; + + // Instantiate the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Initialise the change callback. + let changeCallback = function(changed) { + do_print(changed + " has changed."); + + detectedChanges += 1; + + // Resolve the promise if we get all the expected changes. + if (detectedChanges >= expectedChanges) { + deferred.resolve(); + } + }; + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching it. + yield promiseAddPath(watcher, watchedDir, changeCallback, deferred.reject); + + // Create and then remove the files within the watched directory. + for (let i = 0; i < numberOfFiles; i++) { + let tmpFilePath = OS.Path.join(watchedDir, fileNameBase + i); + yield OS.File.writeAtomic(tmpFilePath, "test content"); + yield OS.File.remove(tmpFilePath); + } + + // Wait until the watcher informs us that all the files were + // created, modified and removed. + yield deferred.promise; + + // Remove the watch and free the associated memory (we need to + // reuse 'changeCallback' and 'errorCallback' to unregister). + yield promiseRemovePath(watcher, watchedDir, changeCallback, deferred.reject); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js new file mode 100644 index 000000000..c55b262f1 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_multi_paths.js @@ -0,0 +1,114 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files + do_get_profile(); + + // Start executing the tests + run_next_test(); +} + +/** + * Tests the watcher by watching several resources. + * This test creates the specified number of directory inside the profile + * directory, adds each one of them to the watched list the creates + * a file in them in order to trigger the notification. + * The test keeps track of the number of times the changes callback is + * called in order to verify the success of the test. + */ +add_task(function* test_watch_multi_paths() { + + // The number of resources to watch. We expect changes for + // creating a file within each directory. + let resourcesToWatch = 5; + let watchedDir = OS.Constants.Path.profileDir; + + // The directories to be watched will be created with. + let tempDirNameBase = "FileWatcher_Test_"; + let tempFileName = "test.tmp"; + + // Instantiate the native watcher. + let watcher = makeWatcher(); + + // This will be used to keep track of the number of changes within the watched + // resources. + let detectedChanges = 0; + let watchedResources = 0; + let unwatchedResources = 0; + + let deferredChanges = Promise.defer(); + let deferredSuccesses = Promise.defer(); + let deferredShutdown = Promise.defer(); + + // Define the change callback function. + let changeCallback = function(changed) { + do_print(changed + " has changed."); + + detectedChanges += 1; + + // Resolve the promise if we get all the expected changes. + if (detectedChanges === resourcesToWatch) { + deferredChanges.resolve(); + } + }; + + // Define the watch success callback function. + let watchSuccessCallback = function(resourcePath) { + do_print(resourcePath + " is being watched."); + + watchedResources += 1; + + // Resolve the promise when all the resources are being + // watched. + if (watchedResources === resourcesToWatch) { + deferredSuccesses.resolve(); + } + }; + + // Define the watch success callback function. + let unwatchSuccessCallback = function(resourcePath) { + do_print(resourcePath + " is being un-watched."); + + unwatchedResources += 1; + + // Resolve the promise when all the resources are being + // watched. + if (unwatchedResources === resourcesToWatch) { + deferredShutdown.resolve(); + } + }; + + // Create the directories and add them to the watched resources list. + for (let i = 0; i < resourcesToWatch; i++) { + let tmpSubDirPath = OS.Path.join(watchedDir, tempDirNameBase + i); + do_print("Creating the " + tmpSubDirPath + " directory."); + yield OS.File.makeDir(tmpSubDirPath); + watcher.addPath(tmpSubDirPath, changeCallback, deferredChanges.reject, watchSuccessCallback); + } + + // Wait until the watcher informs us that all the desired resources + // are being watched. + yield deferredSuccesses.promise; + + // Create a file within each watched directory. + for (let i = 0; i < resourcesToWatch; i++) { + let tmpFilePath = OS.Path.join(watchedDir, tempDirNameBase + i, tempFileName); + yield OS.File.writeAtomic(tmpFilePath, "test content"); + } + + // Wait until the watcher informs us that all the files were created. + yield deferredChanges.promise; + + // Remove the directories we have created. + for (let i = 0; i < resourcesToWatch; i++) { + let tmpSubDirPath = OS.Path.join(watchedDir, tempDirNameBase + i); + watcher.removePath(tmpSubDirPath, changeCallback, deferredChanges.reject, unwatchSuccessCallback); + } + + // Wait until the watcher un-watches the resources. + yield deferredShutdown.promise; +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.js new file mode 100644 index 000000000..13a3de8d3 --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_recursively.js @@ -0,0 +1,55 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files. + do_get_profile(); + + // Start executing the tests. + run_next_test(); +} + +/** + * Test the watcher correctly notifies of a file creation in a subdirectory + * of the watched sub-directory (recursion). + */ +add_task(function* test_watch_recursively() { + + // Create and watch a sub-directory of the profile directory so we don't + // catch notifications we're not interested in (i.e. "startupCache"). + let watchedDir = OS.Path.join(OS.Constants.Path.profileDir, "filewatcher_playground"); + yield OS.File.makeDir(watchedDir); + + // We need at least 2 levels of directories to test recursion. + let subdirectory = OS.Path.join(watchedDir, "level1"); + yield OS.File.makeDir(subdirectory); + + let tempFileName = "test_filecreation.tmp"; + + // Instantiate and initialize the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + let tmpFilePath = OS.Path.join(subdirectory, tempFileName); + + // Add the profile directory to the watch list and wait for the file watcher + // to start watching it. + yield promiseAddPath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Create a file within the subdirectory of the watched directory. + yield OS.File.writeAtomic(tmpFilePath, "some data"); + + // Wait until the watcher informs us that the file was created. + let changed = yield deferred.promise; + do_check_eq(changed, tmpFilePath); + + // Remove the watch and free the associated memory (we need to + // reuse 'deferred.resolve' and 'deferred.reject' to unregister). + yield promiseRemovePath(watcher, watchedDir, deferred.resolve, deferred.reject); + + // Remove the test directory and all of its content. + yield OS.File.removeDir(watchedDir); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js b/toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js new file mode 100644 index 000000000..fffdff24b --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/test_watch_resource.js @@ -0,0 +1,32 @@ +/* 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/. */ + +"use strict"; + +function run_test() { + // Set up profile. We will use profile path create some test files + do_get_profile(); + + // Start executing the tests + run_next_test(); +} + +/** + * Test watching non-existing path + */ +add_task(function* test_watching_non_existing() { + let notExistingDir = + OS.Path.join(OS.Constants.Path.profileDir, "absolutelyNotExisting"); + + // Instantiate the native watcher. + let watcher = makeWatcher(); + let deferred = Promise.defer(); + + // Try watch a path which doesn't exist. + watcher.addPath(notExistingDir, deferred.reject, deferred.resolve); + + // Wait until the watcher informs us that there was an error. + let error = yield deferred.promise; + do_check_eq(error, Components.results.NS_ERROR_FILE_NOT_FOUND); +}); diff --git a/toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini b/toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini new file mode 100644 index 000000000..d6cc968eb --- /dev/null +++ b/toolkit/components/filewatcher/tests/xpcshell/xpcshell.ini @@ -0,0 +1,18 @@ +[DEFAULT] +head = head.js +tail = +skip-if = os != "win" + +[test_arguments.js] +[test_no_error_callback.js] +[test_remove_non_watched.js] +[test_shared_callback.js] +[test_watch_file_creation_single.js] +[test_watch_file_deletion_single.js] +[test_watch_file_modification_single.js] +[test_watch_directory_creation_single.js] +[test_watch_directory_deletion_single.js] +[test_watch_many_changes.js] +[test_watch_multi_paths.js] +[test_watch_recursively.js] +[test_watch_resource.js] |