mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-05 16:46:26 +00:00
1515 lines
52 KiB
C++
1515 lines
52 KiB
C++
/* 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 "prlog.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 MOZ_FINAL : public nsRunnable
|
|
{
|
|
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_METHOD Run()
|
|
{
|
|
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 MOZ_FINAL : public nsRunnable
|
|
{
|
|
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_METHOD Run()
|
|
{
|
|
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 MOZ_FINAL : public nsRunnable
|
|
{
|
|
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_METHOD Run()
|
|
{
|
|
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;
|
|
};
|
|
|
|
#if defined(PR_LOGGING)
|
|
static PRLogModuleInfo* GetFileWatcherContextLog()
|
|
{
|
|
static PRLogModuleInfo *gNativeWatcherPRLog;
|
|
if (!gNativeWatcherPRLog) {
|
|
gNativeWatcherPRLog = PR_NewLogModule("NativeFileWatcherService");
|
|
}
|
|
return gNativeWatcherPRLog;
|
|
}
|
|
#endif
|
|
|
|
#define FILEWATCHERLOG(...) PR_LOG(GetFileWatcherContextLog(), PR_LOG_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 nsRunnable
|
|
{
|
|
public:
|
|
NativeWatcherIOShutdownTask()
|
|
: mWorkerThread(do_GetCurrentThread())
|
|
{
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
}
|
|
|
|
NS_IMETHOD Run()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
mWorkerThread->Shutdown();
|
|
return NS_OK;
|
|
}
|
|
|
|
private:
|
|
nsCOMPtr<nsIThread> mWorkerThread;
|
|
};
|
|
|
|
/**
|
|
* An helper callback function used to print information about any
|
|
* pending watch when shutting down the nsINativeFileWatcher service.
|
|
*/
|
|
static PLDHashOperator
|
|
WatchedPathsInfoHashtableTraverser(nsVoidPtrHashKey::KeyType key,
|
|
WatchedResourceDescriptor* watchedResource,
|
|
void* userArg)
|
|
{
|
|
FILEWATCHERLOG("NativeFileWatcherIOTask::DeactivateRunnableMethod - "
|
|
"%S is still being watched.", watchedResource->mPath.get());
|
|
|
|
return PL_DHASH_NEXT;
|
|
}
|
|
|
|
/**
|
|
* 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 nsRunnable
|
|
{
|
|
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.
|
|
(void)mWatchedResourcesByHandle.EnumerateRead(
|
|
&WatchedPathsInfoHashtableTraverser, nullptr);
|
|
|
|
// 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.
|
|
nsRefPtr<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)
|
|
{
|
|
nsRefPtr<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)
|
|
{
|
|
nsRefPtr<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)
|
|
{
|
|
nsRefPtr<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;
|
|
}
|
|
|
|
} // anonymous 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 NS_NewRunnableMethodWithArg.
|
|
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(
|
|
NS_NewRunnableMethodWithArg<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 NS_NewRunnableMethodWithArg.
|
|
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(
|
|
NS_NewRunnableMethodWithArg<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(
|
|
NS_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
|