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..701d5ad13100 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,161 @@ 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; + } + + if (mBlockerStatus != ShutdownBlockerStatus::Initialized) { + NS_WARNING("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 +1624,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 +1890,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 1cd6ef213c9a..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,27 +148,20 @@ class IOUtils final { struct InternalFileInfo; struct InternalWriteOpts; class MozLZ4; + class EventQueue; + class State; - static StaticDataMutex> - sBackgroundEventTarget; - static StaticRefPtr sBarrier; - static Atomic sShutdownStarted; - - template - static RefPtr> InvokeToIOPromise(Fn aFunc, - Args... aArgs); - - 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. @@ -176,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. * @@ -369,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; }; /** @@ -481,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/gfx/2d/MacIOSurface.cpp b/gfx/2d/MacIOSurface.cpp index 0a410cf14afa..bae93316270b 100644 --- a/gfx/2d/MacIOSurface.cpp +++ b/gfx/2d/MacIOSurface.cpp @@ -14,6 +14,7 @@ #include "mozilla/Assertions.h" #include "mozilla/RefPtr.h" #include "mozilla/gfx/Logging.h" +#include "mozilla/StaticPrefs_gfx.h" using namespace mozilla; @@ -84,6 +85,11 @@ already_AddRefed MacIOSurface::CreateIOSurface( CFTypeRefPtr::WrapUnderCreateRule( ::IOSurfaceCreate(props.get())); + if (StaticPrefs::gfx_color_management_native_srgb()) { + IOSurfaceSetValue(surfaceRef.get(), CFSTR("IOSurfaceColorSpace"), + kCGColorSpaceSRGB); + } + if (!surfaceRef) { return nullptr; } diff --git a/gfx/2d/SourceSurfaceD2D1.cpp b/gfx/2d/SourceSurfaceD2D1.cpp index 659b500c59bb..2be9f9289de3 100644 --- a/gfx/2d/SourceSurfaceD2D1.cpp +++ b/gfx/2d/SourceSurfaceD2D1.cpp @@ -17,17 +17,22 @@ SourceSurfaceD2D1::SourceSurfaceD2D1(ID2D1Image* aImage, : mImage(aImage), mDC(aDC), mDevice(Factory::GetD2D1Device()), - mDrawTarget(aDT) { + mFormat(aFormat), + mSize(aSize), + mDrawTarget(aDT), + mOwnsCopy(false) { aImage->QueryInterface((ID2D1Bitmap1**)getter_AddRefs(mRealizedBitmap)); - - mFormat = aFormat; - mSize = aSize; if (aDT) { mSnapshotLock = aDT->mSnapshotLock; } } -SourceSurfaceD2D1::~SourceSurfaceD2D1() {} +SourceSurfaceD2D1::~SourceSurfaceD2D1() { + if (mOwnsCopy) { + DrawTargetD2D1::mVRAMUsageSS -= + mSize.width * mSize.height * BytesPerPixel(mFormat); + } +} bool SourceSurfaceD2D1::IsValid() const { return mDevice == Factory::GetD2D1Device(); @@ -145,6 +150,7 @@ void SourceSurfaceD2D1::DrawTargetWillChange() { DrawTargetD2D1::mVRAMUsageSS += mSize.width * mSize.height * BytesPerPixel(mFormat); + mOwnsCopy = true; // Ensure the object stays alive for the duration of MarkIndependent. RefPtr deathGrip = this; diff --git a/gfx/2d/SourceSurfaceD2D1.h b/gfx/2d/SourceSurfaceD2D1.h index 7d2ce10805aa..cfc6dd6b86a8 100644 --- a/gfx/2d/SourceSurfaceD2D1.h +++ b/gfx/2d/SourceSurfaceD2D1.h @@ -62,10 +62,11 @@ class SourceSurfaceD2D1 : public SourceSurface { // Keep this around to verify whether out image is still valid in the future. RefPtr mDevice; - SurfaceFormat mFormat; - IntSize mSize; + const SurfaceFormat mFormat; + const IntSize mSize; DrawTargetD2D1* mDrawTarget; std::shared_ptr mSnapshotLock; + bool mOwnsCopy; }; class DataSourceSurfaceD2D1 : public DataSourceSurface { diff --git a/gfx/layers/SurfacePoolCA.mm b/gfx/layers/SurfacePoolCA.mm index d556f488933b..424fbfbce488 100644 --- a/gfx/layers/SurfacePoolCA.mm +++ b/gfx/layers/SurfacePoolCA.mm @@ -164,6 +164,10 @@ CFTypeRefPtr SurfacePoolCA::LockedPool::ObtainSurfaceFromPool(cons (__bridge NSString*)kIOSurfaceBytesPerElement : @(4), })); if (surface) { + if (StaticPrefs::gfx_color_management_native_srgb()) { + IOSurfaceSetValue(surface.get(), CFSTR("IOSurfaceColorSpace"), + kCGColorSpaceSRGB); + } // Create a new entry in mInUseEntries. MutateEntryStorage("Create", aSize, [&]() { mInUseEntries.insert({surface, SurfacePoolEntry{aSize, surface, {}}}); diff --git a/gfx/thebes/gfxPlatform.cpp b/gfx/thebes/gfxPlatform.cpp index 2492b73c254f..9fd5da975131 100644 --- a/gfx/thebes/gfxPlatform.cpp +++ b/gfx/thebes/gfxPlatform.cpp @@ -2152,7 +2152,8 @@ void gfxPlatform::InitializeCMS() { of this preference, which means nsIPrefBranch::GetBoolPref will typically throw (and leave its out-param untouched). */ - if (StaticPrefs::gfx_color_management_force_srgb()) { + if (StaticPrefs::gfx_color_management_force_srgb() || + StaticPrefs::gfx_color_management_native_srgb()) { gCMSOutputProfile = gCMSsRGBProfile; } diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt index e0d599e3d7f7..6630c4269b72 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TextInputDelegateTest.kt @@ -933,7 +933,7 @@ class TextInputDelegateTest : BaseSessionTest() { val ic = mainSession.textInput.onCreateInputConnection(EditorInfo())!! commitText(ic, "foo", 1) - setSelection(ic, 0, 3) + ic.setSelection(0, 3) mainSession.evaluateJS(""" input_event_count = 0; diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 8d6cb79775c0..8ac3a655bca4 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -4191,6 +4191,15 @@ value: false mirror: always +- name: gfx.color_management.native_srgb + type: RelaxedAtomicBool +#if defined(XP_MACOSX) + value: true +#else + value: false +#endif + mirror: always + - name: gfx.color_management.enablev4 type: RelaxedAtomicBool value: false diff --git a/toolkit/components/telemetry/app/ClientID.jsm b/toolkit/components/telemetry/app/ClientID.jsm index bae320e9f9d2..2a09418ef55a 100644 --- a/toolkit/components/telemetry/app/ClientID.jsm +++ b/toolkit/components/telemetry/app/ClientID.jsm @@ -297,7 +297,7 @@ var ClientIDImpl = { try { await IOUtils.makeDirectory(gDatareportingPath); } catch (ex) { - if (ex.name != "NotAllowedError") { + if (!(ex instanceof DOMException) || ex.name !== "AbortError") { throw ex; } } diff --git a/widget/cocoa/nsLookAndFeel.h b/widget/cocoa/nsLookAndFeel.h index 6d2ef98bc4fe..b0189c37b95b 100644 --- a/widget/cocoa/nsLookAndFeel.h +++ b/widget/cocoa/nsLookAndFeel.h @@ -57,23 +57,17 @@ class nsLookAndFeel final : public nsXPLookAndFeel { nscolor mColorTextSelectBackgroundDisabled; nscolor mColorHighlight; nscolor mColorTextSelectForeground; - nscolor mColorMenuHoverText; - nscolor mColorButtonText; - nscolor mColorButtonHoverText; + nscolor mColorAlternateSelectedControlText; + nscolor mColorControlText; nscolor mColorText; nscolor mColorWindowText; - nscolor mColorActiveCaption; + nscolor mColorGrid; nscolor mColorActiveBorder; nscolor mColorGrayText; - nscolor mColorInactiveBorder; - nscolor mColorInactiveCaption; + nscolor mColorControlBackground; nscolor mColorScrollbar; nscolor mColorThreeDHighlight; - nscolor mColorMenu; - nscolor mColorWindowFrame; - nscolor mColorFieldText; nscolor mColorDialog; - nscolor mColorDialogText; nscolor mColorDragTargetZone; nscolor mColorChromeActive; nscolor mColorChromeInactive; diff --git a/widget/cocoa/nsLookAndFeel.mm b/widget/cocoa/nsLookAndFeel.mm index b577402b9182..848cf651616e 100644 --- a/widget/cocoa/nsLookAndFeel.mm +++ b/widget/cocoa/nsLookAndFeel.mm @@ -60,23 +60,17 @@ nsLookAndFeel::nsLookAndFeel(const LookAndFeelCache* aCache) mColorTextSelectBackgroundDisabled(0), mColorHighlight(0), mColorTextSelectForeground(0), - mColorMenuHoverText(0), - mColorButtonText(0), - mColorButtonHoverText(0), + mColorAlternateSelectedControlText(0), + mColorControlText(0), mColorText(0), mColorWindowText(0), - mColorActiveCaption(0), + mColorGrid(0), mColorActiveBorder(0), mColorGrayText(0), - mColorInactiveBorder(0), - mColorInactiveCaption(0), + mColorControlBackground(0), mColorScrollbar(0), mColorThreeDHighlight(0), - mColorMenu(0), - mColorWindowFrame(0), - mColorFieldText(0), mColorDialog(0), - mColorDialogText(0), mColorDragTargetZone(0), mColorChromeActive(0), mColorChromeInactive(0), @@ -222,7 +216,7 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { case ColorID::Highlighttext: // CSS2 color case ColorID::MozAccentColorForeground: case ColorID::MozMenuhovertext: - aColor = mColorMenuHoverText; + aColor = mColorAlternateSelectedControlText; break; case ColorID::IMESelectedRawTextBackground: case ColorID::IMESelectedConvertedTextBackground: @@ -261,11 +255,11 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { // case ColorID::MozMacButtonactivetext: case ColorID::MozMacDefaultbuttontext: - aColor = mColorButtonText; + aColor = NS_RGB(0xFF, 0xFF, 0xFF); break; case ColorID::Buttontext: case ColorID::MozButtonhovertext: - aColor = mColorButtonHoverText; + aColor = mColorControlText; break; case ColorID::Captiontext: case ColorID::Menutext: @@ -277,7 +271,7 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { aColor = mColorWindowText; break; case ColorID::Activecaption: - aColor = mColorActiveCaption; + aColor = mColorGrid; break; case ColorID::Activeborder: aColor = mColorActiveBorder; @@ -302,10 +296,8 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { aColor = mColorGrayText; break; case ColorID::Inactiveborder: - aColor = mColorInactiveBorder; - break; case ColorID::Inactivecaption: - aColor = mColorInactiveCaption; + aColor = mColorControlBackground; break; case ColorID::Inactivecaptiontext: aColor = NS_RGB(0x45, 0x45, 0x45); @@ -329,10 +321,10 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { aColor = NS_RGB(0xDA, 0xDA, 0xDA); break; case ColorID::Menu: - aColor = mColorMenu; + aColor = mColorAlternateSelectedControlText; break; case ColorID::Windowframe: - aColor = mColorWindowFrame; + aColor = mColorGrid; break; case ColorID::Window: case ColorID::Field: @@ -341,7 +333,7 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { break; case ColorID::Fieldtext: case ColorID::MozComboboxtext: - aColor = mColorFieldText; + aColor = mColorControlText; break; case ColorID::MozDialog: aColor = mColorDialog; @@ -351,7 +343,7 @@ nsresult nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) { case ColorID::MozHtmlCellhighlighttext: case ColorID::MozColheadertext: case ColorID::MozColheaderhovertext: - aColor = mColorDialogText; + aColor = mColorControlText; break; case ColorID::MozDragtargetzone: aColor = mColorDragTargetZone; @@ -697,21 +689,15 @@ mozilla::widget::LookAndFeelCache nsLookAndFeel::GetCacheImpl() { ColorID::TextSelectBackground, ColorID::TextSelectBackgroundDisabled, ColorID::TextSelectForeground, - ColorID::MozMacDefaultbuttontext, - ColorID::MozButtonhovertext, ColorID::Windowtext, ColorID::Activecaption, ColorID::Activeborder, ColorID::Graytext, ColorID::Inactiveborder, - ColorID::Inactivecaption, ColorID::Scrollbar, ColorID::Threedhighlight, - ColorID::Menu, - ColorID::Windowframe, ColorID::Fieldtext, ColorID::MozDialog, - ColorID::MozDialogtext, ColorID::MozDragtargetzone, ColorID::MozMacChromeActive, ColorID::MozMacChromeInactive, @@ -777,7 +763,7 @@ void nsLookAndFeel::DoSetCache(const LookAndFeelCache& aCache) { case ColorID::Highlight: return mColorHighlight; case ColorID::Highlighttext: - return mColorMenuHoverText; + return mColorAlternateSelectedControlText; case ColorID::Menutext: return mColorText; case ColorID::TextSelectBackground: @@ -786,36 +772,24 @@ void nsLookAndFeel::DoSetCache(const LookAndFeelCache& aCache) { return mColorTextSelectBackgroundDisabled; case ColorID::TextSelectForeground: return mColorTextSelectForeground; - case ColorID::MozMacDefaultbuttontext: - return mColorButtonText; - case ColorID::MozButtonhovertext: - return mColorButtonHoverText; case ColorID::Windowtext: return mColorWindowText; case ColorID::Activecaption: - return mColorActiveCaption; + return mColorGrid; case ColorID::Activeborder: return mColorActiveBorder; case ColorID::Graytext: return mColorGrayText; case ColorID::Inactiveborder: - return mColorInactiveBorder; - case ColorID::Inactivecaption: - return mColorInactiveCaption; + return mColorControlBackground; case ColorID::Scrollbar: return mColorScrollbar; case ColorID::Threedhighlight: return mColorThreeDHighlight; - case ColorID::Menu: - return mColorMenu; - case ColorID::Windowframe: - return mColorWindowFrame; case ColorID::Fieldtext: - return mColorFieldText; + return mColorControlText; case ColorID::MozDialog: return mColorDialog; - case ColorID::MozDialogtext: - return mColorDialogText; case ColorID::MozDragtargetzone: return mColorDragTargetZone; case ColorID::MozMacChromeActive: @@ -893,25 +867,20 @@ void nsLookAndFeel::EnsureInit() { mColorTextSelectForeground = NS_DONT_CHANGE_COLOR; } - mColorMenuHoverText = GetColorFromNSColor([NSColor alternateSelectedControlTextColor]); + mColorAlternateSelectedControlText = + GetColorFromNSColor([NSColor alternateSelectedControlTextColor]); - mColorButtonText = NS_RGB(0xFF, 0xFF, 0xFF); - mColorButtonHoverText = GetColorFromNSColor([NSColor controlTextColor]); + mColorControlText = GetColorFromNSColor([NSColor controlTextColor]); mColorText = GetColorFromNSColor([NSColor textColor]); mColorWindowText = GetColorFromNSColor([NSColor windowFrameTextColor]); - mColorActiveCaption = GetColorFromNSColor([NSColor gridColor]); + mColorGrid = GetColorFromNSColor([NSColor gridColor]); mColorActiveBorder = GetColorFromNSColor([NSColor keyboardFocusIndicatorColor]); NSColor* disabledColor = [NSColor disabledControlTextColor]; mColorGrayText = GetColorFromNSColorWithAlpha(disabledColor, [disabledColor alphaComponent]); - mColorInactiveBorder = GetColorFromNSColor([NSColor controlBackgroundColor]); - mColorInactiveCaption = GetColorFromNSColor([NSColor controlBackgroundColor]); + mColorControlBackground = GetColorFromNSColor([NSColor controlBackgroundColor]); mColorScrollbar = GetColorFromNSColor([NSColor scrollBarColor]); mColorThreeDHighlight = GetColorFromNSColor([NSColor highlightColor]); - mColorMenu = GetColorFromNSColor([NSColor alternateSelectedControlTextColor]); - mColorWindowFrame = GetColorFromNSColor([NSColor gridColor]); - mColorFieldText = GetColorFromNSColor([NSColor controlTextColor]); mColorDialog = GetColorFromNSColor([NSColor controlHighlightColor]); - mColorDialogText = GetColorFromNSColor([NSColor controlTextColor]); mColorDragTargetZone = GetColorFromNSColor([NSColor selectedControlColor]); int grey = NativeGreyColorAsInt(toolbarFillGrey, true);