diff --git a/dom/chrome-webidl/IOUtils.webidl b/dom/chrome-webidl/IOUtils.webidl index 6ce51a66c900..13c80ca71370 100644 --- a/dom/chrome-webidl/IOUtils.webidl +++ b/dom/chrome-webidl/IOUtils.webidl @@ -206,6 +206,12 @@ namespace IOUtils { Promise exists(DOMString path); }; +[Exposed=Window] +partial namespace IOUtils { + [Throws] + readonly attribute any profileBeforeChange; +}; + /** * Options to be passed to the |IOUtils.readUTF8| method. */ diff --git a/dom/system/IOUtils.cpp b/dom/system/IOUtils.cpp index 1e1b637ce4d8..621d52c71696 100644 --- a/dom/system/IOUtils.cpp +++ b/dom/system/IOUtils.cpp @@ -14,6 +14,7 @@ #include "js/Utility.h" #include "js/experimental/TypedData.h" #include "jsfriendapi.h" +#include "mozilla/AutoRestore.h" #include "mozilla/Compression.h" #include "mozilla/Encoding.h" #include "mozilla/EndianUtils.h" @@ -34,6 +35,7 @@ #include "nsIDirectoryEnumerator.h" #include "nsIFile.h" #include "nsIGlobalObject.h" +#include "nsISupports.h" #include "nsLocalFile.h" #include "nsPrintfCString.h" #include "nsReadableUtils.h" @@ -51,16 +53,6 @@ # include "nsSystemInfo.h" #endif -#define REJECT_IF_SHUTTING_DOWN(aJSPromise) \ - do { \ - if (sShutdownStarted) { \ - (aJSPromise) \ - ->MaybeRejectWithNotAllowedError( \ - "Shutting down and refusing additional I/O tasks"); \ - return (aJSPromise).forget(); \ - } \ - } while (false) - #define REJECT_IF_INIT_PATH_FAILED(_file, _path, _promise) \ do { \ if (nsresult _rv = (_file)->InitWithPath((_path)); NS_FAILED(_rv)) { \ @@ -71,6 +63,9 @@ } \ } while (0) +static constexpr auto SHUTDOWN_ERROR = + "IOUtils: Shutting down and refusing additional I/O tasks"_ns; + namespace mozilla::dom { // static helper functions @@ -148,568 +143,8 @@ static nsCString FormatErrorMessage(nsresult aError, return ToJSValue(aCx, info, aValue); } -// IOUtils implementation - -/* static */ -StaticDataMutex> - IOUtils::sBackgroundEventTarget("sBackgroundEventTarget"); -/* static */ -StaticRefPtr IOUtils::sBarrier; -/* static */ -Atomic IOUtils::sShutdownStarted = Atomic(false); - -/* static */ -template -RefPtr> IOUtils::RunOnBackgroundThread(Fn aFunc) { - nsCOMPtr bg = GetBackgroundEventTarget(); - if (!bg) { - return IOPromise::CreateAndReject( - IOError(NS_ERROR_ABORT) - .WithMessage("Could not dispatch task to background thread"), - __func__); - } - - return InvokeAsync(bg, __func__, [func = std::move(aFunc)]() { - Result result = func(); - if (result.isErr()) { - return IOPromise::CreateAndReject(result.unwrapErr(), __func__); - } - return IOPromise::CreateAndResolve(result.unwrap(), __func__); - }); -} - -/* static */ -template -void IOUtils::RunOnBackgroundThreadAndResolve(Promise* aPromise, Fn aFunc) { - RunOnBackgroundThread(std::move(aFunc)) - ->Then( - GetCurrentSerialEventTarget(), __func__, - [promise = RefPtr(aPromise)](OkT&& ok) { - ResolveJSPromise(promise, std::forward(ok)); - }, - [promise = RefPtr(aPromise)](const IOError& err) { - RejectJSPromise(promise, err); - }); -} - -/* static */ -already_AddRefed IOUtils::Read(GlobalObject& aGlobal, - const nsAString& aPath, - const ReadOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - Maybe toRead = Nothing(); - if (!aOptions.mMaxBytes.IsNull()) { - if (aOptions.mMaxBytes.Value() == 0) { - // Resolve with an empty buffer. - nsTArray arr(0); - promise->MaybeResolve(TypedArrayCreator(arr)); - return promise.forget(); - } - toRead.emplace(aOptions.mMaxBytes.Value()); - } - - RunOnBackgroundThreadAndResolve( - promise, - [file = std::move(file), toRead, decompress = aOptions.mDecompress]() { - return ReadSync(file, toRead, decompress, BufferKind::Uint8Array); - }); - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::ReadUTF8(GlobalObject& aGlobal, - const nsAString& aPath, - const ReadUTF8Options& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file), decompress = aOptions.mDecompress]() { - return ReadUTF8Sync(file, decompress); - }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::ReadJSON(GlobalObject& aGlobal, - const nsAString& aPath, - const ReadUTF8Options& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - RunOnBackgroundThread([file, decompress = aOptions.mDecompress]() { - return ReadUTF8Sync(file, decompress); - }) - ->Then( - GetCurrentSerialEventTarget(), __func__, - [promise, file](JsBuffer&& aBuffer) { - AutoJSAPI jsapi; - if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) { - promise->MaybeRejectWithUnknownError( - "Could not initialize JS API"); - return; - } - JSContext* cx = jsapi.cx(); - - JS::Rooted jsonStr( - cx, IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer))); - if (!jsonStr) { - RejectJSPromise(promise, IOError(NS_ERROR_OUT_OF_MEMORY)); - return; - } - - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, jsonStr, &val)) { - JS::Rooted exn(cx); - if (JS_GetPendingException(cx, &exn)) { - JS_ClearPendingException(cx); - promise->MaybeReject(exn); - } else { - RejectJSPromise( - promise, - IOError(NS_ERROR_DOM_UNKNOWN_ERR) - .WithMessage("ParseJSON threw an uncatchable exception " - "while parsing file(%s)", - file->HumanReadablePath().get())); - } - - return; - } - - promise->MaybeResolve(val); - }, - [promise](const IOError& aErr) { RejectJSPromise(promise, aErr); }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::Write(GlobalObject& aGlobal, - const nsAString& aPath, - const Uint8Array& aData, - const WriteOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - aData.ComputeState(); - auto buf = Buffer::CopyFrom(Span(aData.Data(), aData.Length())); - if (buf.isNothing()) { - promise->MaybeRejectWithOperationError( - "Out of memory: Could not allocate buffer while writing to file"); - return promise.forget(); - } - - auto opts = InternalWriteOpts::FromBinding(aOptions); - if (opts.isErr()) { - RejectJSPromise(promise, opts.unwrapErr()); - return promise.forget(); - } - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file), buf = std::move(*buf), - opts = opts.unwrap()]() { return WriteSync(file, buf, opts); }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::WriteUTF8(GlobalObject& aGlobal, - const nsAString& aPath, - const nsACString& aString, - const WriteOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - auto opts = InternalWriteOpts::FromBinding(aOptions); - if (opts.isErr()) { - RejectJSPromise(promise, opts.unwrapErr()); - return promise.forget(); - } - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file), str = nsCString(aString), - opts = opts.unwrap()]() { - return WriteSync(file, AsBytes(Span(str)), opts); - }); - - return promise.forget(); -} - -static bool AppendJsonAsUtf8(const char16_t* aData, uint32_t aLen, void* aStr) { - nsCString* str = static_cast(aStr); - return AppendUTF16toUTF8(Span(aData, aLen), *str, fallible); -} - -/* static */ -already_AddRefed IOUtils::WriteJSON(GlobalObject& aGlobal, - const nsAString& aPath, - JS::Handle aValue, - const WriteOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - auto opts = InternalWriteOpts::FromBinding(aOptions); - if (opts.isErr()) { - RejectJSPromise(promise, opts.unwrapErr()); - return promise.forget(); - } - - JSContext* cx = aGlobal.Context(); - JS::Rooted rootedValue(cx, aValue); - nsCString utf8Str; - - if (!JS_Stringify(cx, &rootedValue, nullptr, JS::NullHandleValue, - AppendJsonAsUtf8, &utf8Str)) { - JS::Rooted exn(cx, JS::UndefinedValue()); - if (JS_GetPendingException(cx, &exn)) { - JS_ClearPendingException(cx); - promise->MaybeReject(exn); - } else { - RejectJSPromise(promise, - IOError(NS_ERROR_DOM_UNKNOWN_ERR) - .WithMessage("Could not serialize object to JSON")); - } - return promise.forget(); - } - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file), utf8Str = std::move(utf8Str), - opts = opts.unwrap()]() { - return WriteSync(file, AsBytes(Span(utf8Str)), opts); - }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::Move(GlobalObject& aGlobal, - const nsAString& aSourcePath, - const nsAString& aDestPath, - const MoveOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr sourceFile = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); - - nsCOMPtr destFile = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); - - RunOnBackgroundThreadAndResolve( - promise, - [sourceFile = std::move(sourceFile), destFile = std::move(destFile), - noOverwrite = aOptions.mNoOverwrite]() { - return MoveSync(sourceFile, destFile, noOverwrite); - }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::Remove(GlobalObject& aGlobal, - const nsAString& aPath, - const RemoveOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent, - recursive = aOptions.mRecursive]() { - return RemoveSync(file, ignoreAbsent, recursive); - }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::MakeDirectory( - GlobalObject& aGlobal, const nsAString& aPath, - const MakeDirectoryOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - RunOnBackgroundThreadAndResolve( - promise, - [file = std::move(file), createAncestors = aOptions.mCreateAncestors, - ignoreExisting = aOptions.mIgnoreExisting, - permissions = aOptions.mPermissions]() { - return MakeDirectorySync(file, createAncestors, ignoreExisting, - permissions); - }); - - return promise.forget(); -} - -already_AddRefed IOUtils::Stat(GlobalObject& aGlobal, - const nsAString& aPath) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file)]() { return StatSync(file); }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::Copy(GlobalObject& aGlobal, - const nsAString& aSourcePath, - const nsAString& aDestPath, - const CopyOptions& aOptions) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - REJECT_IF_SHUTTING_DOWN(promise); - - nsCOMPtr sourceFile = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); - - nsCOMPtr destFile = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); - - RunOnBackgroundThreadAndResolve( - promise, - [sourceFile = std::move(sourceFile), destFile = std::move(destFile), - noOverwrite = aOptions.mNoOverwrite, recursive = aOptions.mRecursive]() { - return CopySync(sourceFile, destFile, noOverwrite, recursive); - }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::Touch( - GlobalObject& aGlobal, const nsAString& aPath, - const Optional& aModification) { - MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - Maybe newTime = Nothing(); - if (aModification.WasPassed()) { - newTime = Some(aModification.Value()); - } - - RunOnBackgroundThreadAndResolve( - promise, - [file = std::move(file), newTime]() { return TouchSync(file, newTime); }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::GetChildren(GlobalObject& aGlobal, - const nsAString& aPath) { - MOZ_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - RunOnBackgroundThreadAndResolve>( - promise, [file = std::move(file)]() { return GetChildrenSync(file); }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::SetPermissions(GlobalObject& aGlobal, - const nsAString& aPath, - uint32_t aPermissions, - const bool aHonorUmask) { - MOZ_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - -#if defined(XP_UNIX) && !defined(ANDROID) - if (aHonorUmask) { - aPermissions &= ~nsSystemInfo::gUserUmask; - } -#endif - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file), permissions = aPermissions]() { - return SetPermissionsSync(file, permissions); - }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::Exists(GlobalObject& aGlobal, - const nsAString& aPath) { - MOZ_ASSERT(XRE_IsParentProcess()); - RefPtr promise = CreateJSPromise(aGlobal); - if (!promise) { - return nullptr; - } - - nsCOMPtr file = new nsLocalFile(); - REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); - - RunOnBackgroundThreadAndResolve( - promise, [file = std::move(file)]() { return ExistsSync(file); }); - - return promise.forget(); -} - -/* static */ -already_AddRefed IOUtils::GetBackgroundEventTarget() { - if (sShutdownStarted) { - return nullptr; - } - - auto lockedBackgroundEventTarget = sBackgroundEventTarget.Lock(); - if (!lockedBackgroundEventTarget.ref()) { - nsCOMPtr et; - MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( - "IOUtils::BackgroundIOThread", getter_AddRefs(et))); - MOZ_ASSERT(et); - *lockedBackgroundEventTarget = et; - - if (NS_IsMainThread()) { - IOUtils::SetShutdownHooks(); - } else { - nsCOMPtr runnable = NS_NewRunnableFunction( - __func__, []() { IOUtils::SetShutdownHooks(); }); - NS_DispatchToMainThread(runnable.forget()); - } - } - return do_AddRef(*lockedBackgroundEventTarget); -} - -/* static */ -already_AddRefed IOUtils::GetShutdownBarrier() { - MOZ_RELEASE_ASSERT(NS_IsMainThread()); - - if (!sBarrier) { - nsCOMPtr svc = services::GetAsyncShutdownService(); - MOZ_ASSERT(svc); - - nsCOMPtr barrier; - nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier)); - NS_ENSURE_SUCCESS(rv, nullptr); - sBarrier = barrier; - } - return do_AddRef(sBarrier); -} - -/* static */ -void IOUtils::SetShutdownHooks() { - MOZ_RELEASE_ASSERT(NS_IsMainThread()); - - nsCOMPtr barrier = GetShutdownBarrier(); - nsCOMPtr blocker = new IOUtilsShutdownBlocker(); - - nsresult rv = barrier->AddBlocker( - blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, - u"IOUtils: waiting for pending I/O to finish"_ns); - // Adding a new shutdown blocker should only fail if the current shutdown - // phase has completed. Ensure that we have set our shutdown flag to stop - // accepting new I/O tasks in this case. - if (NS_FAILED(rv)) { - sShutdownStarted = true; - } - NS_ENSURE_SUCCESS_VOID(rv); -} - -/* static */ -already_AddRefed IOUtils::CreateJSPromise(GlobalObject& aGlobal) { - ErrorResult er; - nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); - RefPtr promise = Promise::Create(global, er); - if (er.Failed()) { - return nullptr; - } - MOZ_ASSERT(promise); - return do_AddRef(promise); -} - -/* static */ template -void IOUtils::ResolveJSPromise(Promise* aPromise, T&& aValue) { +static void ResolveJSPromise(Promise* aPromise, T&& aValue) { if constexpr (std::is_same_v) { aPromise->MaybeResolveWithUndefined(); } else { @@ -717,8 +152,7 @@ void IOUtils::ResolveJSPromise(Promise* aPromise, T&& aValue) { } } -/* static */ -void IOUtils::RejectJSPromise(Promise* aPromise, const IOError& aError) { +static void RejectJSPromise(Promise* aPromise, const IOUtils::IOError& aError) { const auto& errMsg = aError.Message(); switch (aError.Code()) { @@ -777,6 +211,537 @@ void IOUtils::RejectJSPromise(Promise* aPromise, const IOError& aError) { } } +static void RejectShuttingDown(Promise* aPromise) { + RejectJSPromise(aPromise, + IOUtils::IOError(NS_ERROR_ABORT).WithMessage(SHUTDOWN_ERROR)); +} + +// IOUtils implementation +/* static */ +IOUtils::StateMutex IOUtils::sState{"IOUtils::sState"}; + +/* static */ +template +void IOUtils::DispatchAndResolve(IOUtils::EventQueue* aQueue, Promise* aPromise, + Fn aFunc) { + if (RefPtr> p = aQueue->Dispatch(std::move(aFunc))) { + p->Then( + GetCurrentSerialEventTarget(), __func__, + [promise = RefPtr(aPromise)](OkT&& ok) { + ResolveJSPromise(promise, std::forward(ok)); + }, + [promise = RefPtr(aPromise)](const IOError& err) { + RejectJSPromise(promise, err); + }); + } +} + +/* static */ +already_AddRefed IOUtils::Read(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe toRead = Nothing(); + if (!aOptions.mMaxBytes.IsNull()) { + if (aOptions.mMaxBytes.Value() == 0) { + // Resolve with an empty buffer. + nsTArray arr(0); + promise->MaybeResolve(TypedArrayCreator(arr)); + return promise.forget(); + } + toRead.emplace(aOptions.mMaxBytes.Value()); + } + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), toRead, decompress = aOptions.mDecompress]() { + return ReadSync(file, toRead, decompress, BufferKind::Uint8Array); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::ReadUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::ReadJSON(GlobalObject& aGlobal, + const nsAString& aPath, + const ReadUTF8Options& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + state.ref() + ->mEventQueue + ->Dispatch([file, decompress = aOptions.mDecompress]() { + return ReadUTF8Sync(file, decompress); + }) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise, file](JsBuffer&& aBuffer) { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) { + promise->MaybeRejectWithUnknownError( + "Could not initialize JS API"); + return; + } + JSContext* cx = jsapi.cx(); + + JS::Rooted jsonStr( + cx, IOUtils::JsBuffer::IntoString(cx, std::move(aBuffer))); + if (!jsonStr) { + RejectJSPromise(promise, IOError(NS_ERROR_OUT_OF_MEMORY)); + return; + } + + JS::Rooted val(cx); + if (!JS_ParseJSON(cx, jsonStr, &val)) { + JS::Rooted exn(cx); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise( + promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage( + "ParseJSON threw an uncatchable exception " + "while parsing file(%s)", + file->HumanReadablePath().get())); + } + + return; + } + + promise->MaybeResolve(val); + }, + [promise](const IOError& aErr) { RejectJSPromise(promise, aErr); }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::Write(GlobalObject& aGlobal, + const nsAString& aPath, + const Uint8Array& aData, + const WriteOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + aData.ComputeState(); + auto buf = Buffer::CopyFrom(Span(aData.Data(), aData.Length())); + if (buf.isNothing()) { + promise->MaybeRejectWithOperationError( + "Out of memory: Could not allocate buffer while writing to file"); + return promise.forget(); + } + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return promise.forget(); + } + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), buf = std::move(*buf), + opts = opts.unwrap()]() { return WriteSync(file, buf, opts); }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::WriteUTF8(GlobalObject& aGlobal, + const nsAString& aPath, + const nsACString& aString, + const WriteOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return promise.forget(); + } + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), str = nsCString(aString), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(str)), opts); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +static bool AppendJsonAsUtf8(const char16_t* aData, uint32_t aLen, void* aStr) { + nsCString* str = static_cast(aStr); + return AppendUTF16toUTF8(Span(aData, aLen), *str, fallible); +} + +/* static */ +already_AddRefed IOUtils::WriteJSON(GlobalObject& aGlobal, + const nsAString& aPath, + JS::Handle aValue, + const WriteOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + auto opts = InternalWriteOpts::FromBinding(aOptions); + if (opts.isErr()) { + RejectJSPromise(promise, opts.unwrapErr()); + return promise.forget(); + } + + JSContext* cx = aGlobal.Context(); + JS::Rooted rootedValue(cx, aValue); + nsCString utf8Str; + + if (!JS_Stringify(cx, &rootedValue, nullptr, JS::NullHandleValue, + AppendJsonAsUtf8, &utf8Str)) { + JS::Rooted exn(cx, JS::UndefinedValue()); + if (JS_GetPendingException(cx, &exn)) { + JS_ClearPendingException(cx); + promise->MaybeReject(exn); + } else { + RejectJSPromise(promise, + IOError(NS_ERROR_DOM_UNKNOWN_ERR) + .WithMessage("Could not serialize object to JSON")); + } + return promise.forget(); + } + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), utf8Str = std::move(utf8Str), + opts = opts.unwrap()]() { + return WriteSync(file, AsBytes(Span(utf8Str)), opts); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::Move(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const MoveOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite]() { + return MoveSync(sourceFile, destFile, noOverwrite); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::Remove(GlobalObject& aGlobal, + const nsAString& aPath, + const RemoveOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), ignoreAbsent = aOptions.mIgnoreAbsent, + recursive = aOptions.mRecursive]() { + return RemoveSync(file, ignoreAbsent, recursive); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::MakeDirectory( + GlobalObject& aGlobal, const nsAString& aPath, + const MakeDirectoryOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), createAncestors = aOptions.mCreateAncestors, + ignoreExisting = aOptions.mIgnoreExisting, + permissions = aOptions.mPermissions]() { + return MakeDirectorySync(file, createAncestors, ignoreExisting, + permissions); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +already_AddRefed IOUtils::Stat(GlobalObject& aGlobal, + const nsAString& aPath) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file)]() { return StatSync(file); }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::Copy(GlobalObject& aGlobal, + const nsAString& aSourcePath, + const nsAString& aDestPath, + const CopyOptions& aOptions) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr sourceFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(sourceFile, aSourcePath, promise); + + nsCOMPtr destFile = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(destFile, aDestPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [sourceFile = std::move(sourceFile), destFile = std::move(destFile), + noOverwrite = aOptions.mNoOverwrite, + recursive = aOptions.mRecursive]() { + return CopySync(sourceFile, destFile, noOverwrite, recursive); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::Touch( + GlobalObject& aGlobal, const nsAString& aPath, + const Optional& aModification) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + Maybe newTime = Nothing(); + if (aModification.WasPassed()) { + newTime = Some(aModification.Value()); + } + DispatchAndResolve(state.ref()->mEventQueue, promise, + [file = std::move(file), newTime]() { + return TouchSync(file, newTime); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::GetChildren(GlobalObject& aGlobal, + const nsAString& aPath) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve>( + state.ref()->mEventQueue, promise, + [file = std::move(file)]() { return GetChildrenSync(file); }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::SetPermissions(GlobalObject& aGlobal, + const nsAString& aPath, + uint32_t aPermissions, + const bool aHonorUmask) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + +#if defined(XP_UNIX) && !defined(ANDROID) + if (aHonorUmask) { + aPermissions &= ~nsSystemInfo::gUserUmask; + } +#endif + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file), permissions = aPermissions]() { + return SetPermissionsSync(file, permissions); + }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::Exists(GlobalObject& aGlobal, + const nsAString& aPath) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + RefPtr promise = CreateJSPromise(aGlobal); + if (!promise) { + return nullptr; + } + + if (auto state = GetState()) { + nsCOMPtr file = new nsLocalFile(); + REJECT_IF_INIT_PATH_FAILED(file, aPath, promise); + + DispatchAndResolve( + state.ref()->mEventQueue, promise, + [file = std::move(file)]() { return ExistsSync(file); }); + } else { + RejectShuttingDown(promise); + } + return promise.forget(); +} + +/* static */ +already_AddRefed IOUtils::CreateJSPromise(GlobalObject& aGlobal) { + ErrorResult er; + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr promise = Promise::Create(global, er); + if (er.Failed()) { + return nullptr; + } + MOZ_ASSERT(promise); + return do_AddRef(promise); +} + /* static */ Result IOUtils::ReadSync( nsIFile* aFile, const Maybe& aMaxBytes, const bool aDecompress, @@ -1418,6 +1383,160 @@ Result IOUtils::ExistsSync(nsIFile* aFile) { return exists; } +/* static */ +void IOUtils::GetProfileBeforeChange(GlobalObject& aGlobal, + JS::MutableHandle aClient, + ErrorResult& aRv) { + MOZ_RELEASE_ASSERT(XRE_IsParentProcess()); + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + if (auto state = GetState()) { + MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus != + ShutdownBlockerStatus::Uninitialized); + + if (state.ref()->mBlockerStatus == ShutdownBlockerStatus::Failed) { + aRv.ThrowAbortError("IOUtils: could not register shutdown blockers"); + return; + } + + MOZ_RELEASE_ASSERT(state.ref()->mBlockerStatus == + ShutdownBlockerStatus::Initialized); + auto result = state.ref()->mEventQueue->GetProfileBeforeChangeClient(); + if (result.isErr()) { + aRv.ThrowAbortError("IOUtils: could not get shutdown client"); + return; + } + + RefPtr client = result.unwrap(); + MOZ_RELEASE_ASSERT(client); + if (nsresult rv = client->GetJsclient(aClient); NS_FAILED(rv)) { + aRv.ThrowAbortError("IOUtils: Could not get shutdown jsclient"); + } + return; + } + + aRv.ThrowAbortError( + "IOUtils: profileBeforeChange phase has already finished"); +} + +/* sstatic */ +Maybe IOUtils::GetState() { + auto state = sState.Lock(); + if (state->mQueueStatus == EventQueueStatus::Shutdown) { + return Nothing{}; + } + + if (state->mQueueStatus == EventQueueStatus::Uninitialized) { + MOZ_RELEASE_ASSERT(!state->mEventQueue); + state->mEventQueue = new EventQueue(); + state->mQueueStatus = EventQueueStatus::Initialized; + + MOZ_RELEASE_ASSERT(state->mBlockerStatus == + ShutdownBlockerStatus::Uninitialized); + } + + if (NS_IsMainThread() && + state->mBlockerStatus == ShutdownBlockerStatus::Uninitialized) { + state->SetShutdownHooks(); + } + + return Some(std::move(state)); +} + +IOUtils::EventQueue::EventQueue() { + MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( + "IOUtils::EventQueue", getter_AddRefs(mBackgroundEventTarget))); + + MOZ_RELEASE_ASSERT(mBackgroundEventTarget); +} + +void IOUtils::State::SetShutdownHooks() { + if (mBlockerStatus != ShutdownBlockerStatus::Uninitialized) { + return; + } + + if (NS_WARN_IF(NS_FAILED(mEventQueue->SetShutdownHooks()))) { + mBlockerStatus = ShutdownBlockerStatus::Failed; + } else { + mBlockerStatus = ShutdownBlockerStatus::Initialized; + } + + MOZ_ASSERT(mBlockerStatus == ShutdownBlockerStatus::Initialized, + "IOUtils: could not register shutdown blockers."); +} + +nsresult IOUtils::EventQueue::SetShutdownHooks() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + nsCOMPtr svc = services::GetAsyncShutdownService(); + if (!svc) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr blocker = new IOUtilsShutdownBlocker( + IOUtilsShutdownBlocker::Phase::ProfileBeforeChange); + + nsCOMPtr profileBeforeChange; + MOZ_TRY(svc->GetProfileBeforeChange(getter_AddRefs(profileBeforeChange))); + MOZ_RELEASE_ASSERT(profileBeforeChange); + + MOZ_TRY(profileBeforeChange->AddBlocker( + blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, + u"IOUtils::EventQueue::SetShutdownHooks"_ns)); + + nsCOMPtr xpcomWillShutdown; + MOZ_TRY(svc->GetXpcomWillShutdown(getter_AddRefs(xpcomWillShutdown))); + MOZ_RELEASE_ASSERT(xpcomWillShutdown); + + blocker = new IOUtilsShutdownBlocker( + IOUtilsShutdownBlocker::Phase::XpcomWillShutdown); + MOZ_TRY(xpcomWillShutdown->AddBlocker( + blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, + u"IOUtils::EventQueue::SetShutdownHooks"_ns)); + + MOZ_TRY(svc->MakeBarrier( + u"IOUtils: waiting for profileBeforeChange IO to complete"_ns, + getter_AddRefs(mProfileBeforeChangeBarrier))); + MOZ_RELEASE_ASSERT(mProfileBeforeChangeBarrier); + + return NS_OK; +} + +template +RefPtr> IOUtils::EventQueue::Dispatch(Fn aFunc) { + MOZ_RELEASE_ASSERT(mBackgroundEventTarget); + + return InvokeAsync( + mBackgroundEventTarget, __func__, [func = std::move(aFunc)]() { + Result result = func(); + if (result.isErr()) { + return IOPromise::CreateAndReject(result.unwrapErr(), __func__); + } + return IOPromise::CreateAndResolve(result.unwrap(), __func__); + }); +}; + +Result, nsresult> +IOUtils::EventQueue::GetProfileBeforeChangeClient() { + if (!mProfileBeforeChangeBarrier) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + nsCOMPtr profileBeforeChange; + MOZ_TRY(mProfileBeforeChangeBarrier->GetClient( + getter_AddRefs(profileBeforeChange))); + return profileBeforeChange.forget(); +} + +Result, nsresult> +IOUtils::EventQueue::GetProfileBeforeChangeBarrier() { + if (!mProfileBeforeChangeBarrier) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + + return do_AddRef(mProfileBeforeChangeBarrier); +} + /* static */ Result, IOUtils::IOError> IOUtils::MozLZ4::Compress( Span aUncompressed) { @@ -1504,39 +1623,102 @@ Result IOUtils::MozLZ4::Decompress( return decompressed; } -NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker); +NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker, + nsIAsyncShutdownCompletionCallback); NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) { - aName = u"IOUtils Blocker"_ns; + aName = u"IOUtils Blocker ("_ns; + + switch (mPhase) { + case Phase::ProfileBeforeChange: + aName.Append(u"profile-before-change"_ns); + break; + + case Phase::XpcomWillShutdown: + aName.Append(u"xpcom-will-shutdown"_ns); + break; + + default: + MOZ_CRASH("Unknown shutdown phase"); + } + + aName.Append(')'); return NS_OK; } NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown( nsIAsyncShutdownClient* aBarrierClient) { - nsCOMPtr et = IOUtils::GetBackgroundEventTarget(); + using EventQueueStatus = IOUtils::EventQueueStatus; - IOUtils::sShutdownStarted = true; + MOZ_RELEASE_ASSERT(NS_IsMainThread()); - if (!IOUtils::sBarrier) { - return NS_ERROR_NULL_POINTER; + nsCOMPtr barrier; + + { + auto state = IOUtils::sState.Lock(); + if (state->mQueueStatus == EventQueueStatus::Shutdown) { + // If the blocker for profile-before-change has already run, then the + // event queue is already torn down and we have nothing to do. + + MOZ_RELEASE_ASSERT(mPhase == Phase::XpcomWillShutdown); + MOZ_RELEASE_ASSERT(!state->mEventQueue); + + Unused << NS_WARN_IF(NS_FAILED(aBarrierClient->RemoveBlocker(this))); + mParentClient = nullptr; + + return NS_OK; + } + + MOZ_RELEASE_ASSERT(state->mEventQueue); + + mParentClient = aBarrierClient; + + barrier = + state->mEventQueue->GetProfileBeforeChangeBarrier().unwrapOr(nullptr); } - nsCOMPtr backgroundRunnable = - NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() { - nsCOMPtr mainThreadRunnable = - NS_NewRunnableFunction(__func__, [self = RefPtr(self)]() { - IOUtils::sBarrier->RemoveBlocker(self); + // We cannot barrier->Wait() while holding the mutex because it will lead to + // deadlock. + if (!barrier || NS_WARN_IF(NS_FAILED(barrier->Wait(this)))) { + // If we don't have a barrier, we still need to flush the IOUtils event + // queue and disable task submission. + // + // Likewise, if waiting on the barrier failed, we are going to make our best + // attempt to clean up. + Unused << Done(); + } - auto lockedBackgroundET = IOUtils::sBackgroundEventTarget.Lock(); - *lockedBackgroundET = nullptr; - IOUtils::sBarrier = nullptr; - }); - nsresult rv = NS_DispatchToMainThread(mainThreadRunnable.forget()); - NS_ENSURE_SUCCESS_VOID(rv); - }); + return NS_OK; +} - return et->Dispatch(backgroundRunnable.forget(), - nsIEventTarget::DISPATCH_NORMAL); +NS_IMETHODIMP IOUtilsShutdownBlocker::Done() { + using EventQueueStatus = IOUtils::EventQueueStatus; + + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + auto state = IOUtils::sState.Lock(); + MOZ_RELEASE_ASSERT(state->mEventQueue); + + // This method is called once we have served all shutdown clients. Now we + // flush the remaining IO queue and forbid additional IO requests. + state->mEventQueue->Dispatch([]() { return Ok{}; }) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr(this)]() { + if (self->mParentClient) { + Unused << NS_WARN_IF( + NS_FAILED(self->mParentClient->RemoveBlocker(self))); + self->mParentClient = nullptr; + + auto state = IOUtils::sState.Lock(); + MOZ_RELEASE_ASSERT(state->mEventQueue); + state->mEventQueue = nullptr; + } + }); + + MOZ_RELEASE_ASSERT(state->mQueueStatus == EventQueueStatus::Initialized); + state->mQueueStatus = EventQueueStatus::Shutdown; + + return NS_OK; } NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) { @@ -1707,5 +1889,4 @@ JSObject* IOUtils::JsBuffer::IntoUint8Array(JSContext* aCx, JsBuffer aBuffer) { } // namespace mozilla::dom -#undef REJECT_IF_SHUTTING_DOWN #undef REJECT_IF_INIT_PATH_FAILED diff --git a/dom/system/IOUtils.h b/dom/system/IOUtils.h index 8328b9f11ed9..2bcf8901df13 100644 --- a/dom/system/IOUtils.h +++ b/dom/system/IOUtils.h @@ -14,12 +14,14 @@ #include "mozilla/DataMutex.h" #include "mozilla/MozPromise.h" #include "mozilla/Result.h" +#include "mozilla/StaticPtr.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/IOUtilsBinding.h" #include "mozilla/dom/TypedArray.h" #include "nsIAsyncShutdown.h" #include "nsISerialEventTarget.h" #include "nsPrintfCString.h" +#include "nsProxyRelease.h" #include "nsString.h" #include "nsStringFwd.h" #include "nsTArray.h" @@ -119,6 +121,10 @@ class IOUtils final { static already_AddRefed Exists(GlobalObject& aGlobal, const nsAString& aPath); + static void GetProfileBeforeChange(GlobalObject& aGlobal, + JS::MutableHandle, + ErrorResult& aRv); + class JsBuffer; /** @@ -142,23 +148,20 @@ class IOUtils final { struct InternalFileInfo; struct InternalWriteOpts; class MozLZ4; + class EventQueue; + class State; - static StaticDataMutex> - sBackgroundEventTarget; - static StaticRefPtr sBarrier; - static Atomic sShutdownStarted; - - static already_AddRefed GetShutdownBarrier(); - - static already_AddRefed GetBackgroundEventTarget(); - - static void SetShutdownHooks(); - + /** + * Dispatch a task on the event queue and resolve or reject the associated + * promise based on the result. + * + * @param aPromise The promise corresponding to the task running on the event + * queue. + * @param aFunc The task to run. + */ template - static RefPtr> RunOnBackgroundThread(Fn aFunc); - - template - static void RunOnBackgroundThreadAndResolve(Promise* aPromise, Fn aFunc); + static void DispatchAndResolve(EventQueue* aQueue, Promise* aPromise, + Fn aFunc); /** * Creates a new JS Promise. @@ -172,16 +175,6 @@ class IOUtils final { const InternalFileInfo& aInternalFileInfo, JS::MutableHandle aValue); - /** - * Resolves |aPromise| with an appropriate JS value for |aValue|. - */ - template - static void ResolveJSPromise(Promise* aPromise, T&& aValue); - /** - * Rejects |aPromise| with an appropriate |DOMException| describing |aError|. - */ - static void RejectJSPromise(Promise* aPromise, const IOError& aError); - /** * Attempts to read the entire file at |aPath| into a buffer. * @@ -365,6 +358,80 @@ class IOUtils final { * @return Whether or not the file exists. */ static Result ExistsSync(nsIFile* aFile); + + enum class EventQueueStatus { + Uninitialized, + Initialized, + Shutdown, + }; + + enum class ShutdownBlockerStatus { + Uninitialized, + Initialized, + Failed, + }; + + /** + * Internal IOUtils state. + */ + class State { + public: + StaticAutoPtr mEventQueue; + EventQueueStatus mQueueStatus = EventQueueStatus::Uninitialized; + ShutdownBlockerStatus mBlockerStatus = ShutdownBlockerStatus::Uninitialized; + + /** + * Set up shutdown hooks to free our internals at shutdown. + * + * NB: Must be called on main thread. + */ + void SetShutdownHooks(); + }; + + using StateMutex = StaticDataMutex; + + /** + * Lock the state mutex and return a handle. If shutdown has not yet + * finished, the internals will be constructed if necessary. + * + * @returns A handle to the internal state, which can be used to retrieve the + * event queue. + * If |Some| is returned, |mEventQueue| is guaranteed to be + * initialized. If shutdown has finished, |Nothing| is returned. + */ + static Maybe GetState(); + + static StateMutex sState; +}; + +/** + * The IOUtils event queue. + */ +class IOUtils::EventQueue final { + friend void IOUtils::State::SetShutdownHooks(); + + public: + EventQueue(); + + EventQueue(const EventQueue&) = delete; + EventQueue(EventQueue&&) = delete; + EventQueue& operator=(const EventQueue&) = delete; + EventQueue& operator=(EventQueue&&) = delete; + + template + RefPtr> Dispatch(Fn aFunc); + + Result, nsresult> + GetProfileBeforeChangeClient(); + + Result, nsresult> + GetProfileBeforeChangeBarrier(); + + private: + nsresult SetShutdownHooks(); + + nsCOMPtr mBackgroundEventTarget; + nsCOMPtr mProfileBeforeChangeBarrier; }; /** @@ -477,13 +544,25 @@ class IOUtils::MozLZ4 { Span aFileContents, IOUtils::BufferKind); }; -class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker { +class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker, + public nsIAsyncShutdownCompletionCallback { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIASYNCSHUTDOWNBLOCKER + NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK + + enum Phase { + ProfileBeforeChange, + XpcomWillShutdown, + }; + + explicit IOUtilsShutdownBlocker(Phase aPhase) : mPhase(aPhase) {} private: virtual ~IOUtilsShutdownBlocker() = default; + + Phase mPhase; + RefPtr mParentClient; }; /** diff --git a/toolkit/components/telemetry/app/ClientID.jsm b/toolkit/components/telemetry/app/ClientID.jsm index 6337e6b789cf..7954b9edc6e3 100644 --- a/toolkit/components/telemetry/app/ClientID.jsm +++ b/toolkit/components/telemetry/app/ClientID.jsm @@ -287,7 +287,7 @@ var ClientIDImpl = { try { await IOUtils.makeDirectory(gDatareportingPath); } catch (ex) { - if (ex.name != "NotAllowedError") { + if (!(ex instanceof DOMException) || ex.name !== "AbortError") { throw ex; } }