/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FileSystemSyncAccessHandle.h" #include "fs/FileSystemAsyncCopy.h" #include "fs/FileSystemRequestHandler.h" #include "mozilla/CheckedInt.h" #include "mozilla/ErrorResult.h" #include "mozilla/FixedBufferOutputStream.h" #include "mozilla/MozPromise.h" #include "mozilla/TaskQueue.h" #include "mozilla/dom/FileSystemAccessHandleChild.h" #include "mozilla/dom/FileSystemAccessHandleControlChild.h" #include "mozilla/dom/FileSystemHandleBinding.h" #include "mozilla/dom/FileSystemLog.h" #include "mozilla/dom/FileSystemManager.h" #include "mozilla/dom/FileSystemManagerChild.h" #include "mozilla/dom/FileSystemSyncAccessHandleBinding.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/UnionTypes.h" #include "mozilla/dom/WorkerCommon.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerRef.h" #include "mozilla/dom/fs/IPCRejectReporter.h" #include "mozilla/dom/fs/TargetPtrHolder.h" #include "mozilla/dom/quota/QuotaCommon.h" #include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/ipc/RandomAccessStreamUtils.h" #include "nsNetCID.h" #include "nsStringStream.h" namespace mozilla::dom { namespace { using SizePromise = Int64Promise; const auto CreateAndRejectSizePromise = CreateAndRejectInt64Promise; } // namespace FileSystemSyncAccessHandle::FileSystemSyncAccessHandle( nsIGlobalObject* aGlobal, RefPtr& aManager, mozilla::ipc::RandomAccessStreamParams&& aStreamParams, RefPtr aActor, RefPtr aControlActor, RefPtr aIOTaskQueue, const fs::FileSystemEntryMetadata& aMetadata) : mGlobal(aGlobal), mManager(aManager), mActor(std::move(aActor)), mControlActor(std::move(aControlActor)), mIOTaskQueue(std::move(aIOTaskQueue)), mStreamParams(std::move(aStreamParams)), mMetadata(aMetadata), mState(State::Initial) { LOG(("Created SyncAccessHandle %p", this)); // Connect with the actor directly in the constructor. This way the actor // can call `FileSystemSyncAccessHandle::ClearActor` when we call // `PFileSystemAccessHandleChild::Send__delete__` even when // FileSystemSyncAccessHandle::Create fails, in which case the not yet // fully constructed FileSystemSyncAccessHandle is being destroyed. mActor->SetAccessHandle(this); mControlActor->SetAccessHandle(this); } FileSystemSyncAccessHandle::~FileSystemSyncAccessHandle() { MOZ_ASSERT(!mActor); MOZ_ASSERT(IsClosed()); } // static Result, nsresult> FileSystemSyncAccessHandle::Create( nsIGlobalObject* aGlobal, RefPtr& aManager, mozilla::ipc::RandomAccessStreamParams&& aStreamParams, mozilla::ipc::ManagedEndpoint&& aAccessHandleChildEndpoint, mozilla::ipc::Endpoint&& aAccessHandleControlChildEndpoint, const fs::FileSystemEntryMetadata& aMetadata) { WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); auto accessHandleChild = MakeRefPtr(); QM_TRY(MOZ_TO_RESULT( aManager->ActorStrongRef()->BindPFileSystemAccessHandleEndpoint( std::move(aAccessHandleChildEndpoint), accessHandleChild))); auto accessHandleControlChild = MakeRefPtr(); aAccessHandleControlChildEndpoint.Bind(accessHandleControlChild, workerPrivate->ControlEventTarget()); QM_TRY_UNWRAP(auto streamTransportService, MOZ_TO_RESULT_GET_TYPED(nsCOMPtr, MOZ_SELECT_OVERLOAD(do_GetService), NS_STREAMTRANSPORTSERVICE_CONTRACTID)); RefPtr ioTaskQueue = TaskQueue::Create( streamTransportService.forget(), "FileSystemSyncAccessHandle"); QM_TRY(MOZ_TO_RESULT(ioTaskQueue)); RefPtr result = new FileSystemSyncAccessHandle( aGlobal, aManager, std::move(aStreamParams), std::move(accessHandleChild), std::move(accessHandleControlChild), std::move(ioTaskQueue), aMetadata); auto autoClose = MakeScopeExit([result] { MOZ_ASSERT(result->mState == State::Initial); result->mState = State::Closed; result->mActor->SendClose(); }); workerPrivate->AssertIsOnWorkerThread(); RefPtr workerRef = StrongWorkerRef::Create( workerPrivate, "FileSystemSyncAccessHandle", [result]() { if (result->IsOpen()) { // We don't need to use the result, we just need to begin the closing // process. Unused << result->BeginClose(); } }); QM_TRY(MOZ_TO_RESULT(workerRef)); autoClose.release(); result->mWorkerRef = std::move(workerRef); result->mState = State::Open; return result; } NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemSyncAccessHandle) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemSyncAccessHandle) NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(FileSystemSyncAccessHandle, LastRelease()) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemSyncAccessHandle) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FileSystemSyncAccessHandle) NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) // Don't unlink mManager! NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER if (tmp->IsOpen()) { // We don't need to use the result, we just need to begin the closing // process. Unused << tmp->BeginClose(); } NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FileSystemSyncAccessHandle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END void FileSystemSyncAccessHandle::LastRelease() { // We can't call `FileSystemSyncAccessHandle::Close` here because it may need // to keep FileSystemSyncAccessHandle object alive which isn't possible when // the object is about to be deleted. There are other mechanisms which ensure // that the object is correctly closed before destruction. For example the // object unlinking and the worker shutdown (we get notified about it via the // callback passed to `StrongWorkerRef`) are used to close the object if it // hasn't been closed yet. if (mActor) { PFileSystemAccessHandleChild::Send__delete__(mActor); // `PFileSystemAccessHandleChild::Send__delete__` is supposed to call // `FileSystemAccessHandleChild::ActorDestroy` which in turn calls // `FileSystemSyncAccessHandle::ClearActor`, so `mActor` should be be null // at this point. MOZ_ASSERT(!mActor); } if (mControlActor) { mControlActor->Close(); // `FileSystemAccessHandleControlChild::Close` is supposed to call // `FileSystemAccessHandleControlChild::ActorDestroy` which in turn calls // `FileSystemSyncAccessHandle::ClearControlActor`, so `mControlActor` // should be be null at this point. MOZ_ASSERT(!mControlActor); } } void FileSystemSyncAccessHandle::ClearActor() { MOZ_ASSERT(mActor); mActor = nullptr; } void FileSystemSyncAccessHandle::ClearControlActor() { // `mControlActor` is initialized in the constructor and this method is // supposed to be called only once. MOZ_ASSERT(mControlActor); mControlActor = nullptr; } bool FileSystemSyncAccessHandle::IsOpen() const { MOZ_ASSERT(mState != State::Initial); return mState == State::Open; } bool FileSystemSyncAccessHandle::IsClosing() const { MOZ_ASSERT(mState != State::Initial); return mState == State::Closing; } bool FileSystemSyncAccessHandle::IsClosed() const { MOZ_ASSERT(mState != State::Initial); return mState == State::Closed; } RefPtr FileSystemSyncAccessHandle::BeginClose() { MOZ_ASSERT(IsOpen()); mState = State::Closing; InvokeAsync(mIOTaskQueue, __func__, [selfHolder = fs::TargetPtrHolder(this)]() { if (selfHolder->mStream) { LOG(("%p: Closing", selfHolder->mStream.get())); selfHolder->mStream->OutputStream()->Close(); selfHolder->mStream = nullptr; } else { LOG(("Closing (no stream)")); // If the stream was not deserialized, `mStreamParams` still // contains a pre-opened file descriptor which needs to be // closed here by moving `mStreamParams` to a local variable // (the file descriptor will be closed for real when // `streamParams` goes out of scope). mozilla::ipc::RandomAccessStreamParams streamParams( std::move(selfHolder->mStreamParams)); } return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(mWorkerRef->Private()->ControlEventTarget(), __func__, [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) { return self->mIOTaskQueue->BeginShutdown(); }) ->Then( mWorkerRef->Private()->ControlEventTarget(), __func__, [self = RefPtr(this)](const ShutdownPromise::ResolveOrRejectValue&) { if (self->mControlActor) { RefPtr promise = new BoolPromise::Private(__func__); self->mControlActor->SendClose( [promise](void_t&&) { promise->Resolve(true, __func__); }, [promise](const mozilla::ipc::ResponseRejectReason& aReason) { fs::IPCRejectReporter(aReason); promise->Reject(NS_ERROR_FAILURE, __func__); }); return RefPtr(promise); } return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(mWorkerRef->Private()->ControlEventTarget(), __func__, [self = RefPtr(this)](const BoolPromise::ResolveOrRejectValue&) { self->mWorkerRef = nullptr; self->mState = State::Closed; self->mClosePromiseHolder.ResolveIfExists(true, __func__); }); return OnClose(); } RefPtr FileSystemSyncAccessHandle::OnClose() { MOZ_ASSERT(mState == State::Closing); return mClosePromiseHolder.Ensure(__func__); } // WebIDL Boilerplate nsIGlobalObject* FileSystemSyncAccessHandle::GetParentObject() const { return mGlobal; } JSObject* FileSystemSyncAccessHandle::WrapObject( JSContext* aCx, JS::Handle aGivenProto) { return FileSystemSyncAccessHandle_Binding::Wrap(aCx, this, aGivenProto); } // WebIDL Interface uint64_t FileSystemSyncAccessHandle::Read( const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer, const FileSystemReadWriteOptions& aOptions, ErrorResult& aRv) { return ReadOrWrite(aBuffer, aOptions, /* aRead */ true, aRv); } uint64_t FileSystemSyncAccessHandle::Write( const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer, const FileSystemReadWriteOptions& aOptions, ErrorResult& aRv) { return ReadOrWrite(aBuffer, aOptions, /* aRead */ false, aRv); } void FileSystemSyncAccessHandle::Truncate(uint64_t aSize, ErrorResult& aError) { if (!IsOpen()) { aError.ThrowInvalidStateError("SyncAccessHandle is closed"); return; } MOZ_ASSERT(mWorkerRef); AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aError](nsresult) { aError.ThrowInvalidStateError("Worker is shutting down"); }); InvokeAsync( mIOTaskQueue, __func__, [selfHolder = fs::TargetPtrHolder(this), aSize]() { QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), CreateAndRejectBoolPromise); LOG(("%p: Truncate to %" PRIu64, selfHolder->mStream.get(), aSize)); int64_t offset = 0; QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Tell(&offset)), CreateAndRejectBoolPromise); QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek( nsISeekableStream::NS_SEEK_SET, aSize)), CreateAndRejectBoolPromise); QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->SetEOF()), CreateAndRejectBoolPromise); // restore cursor position (clamp to end of file) QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek( nsISeekableStream::NS_SEEK_SET, std::min((uint64_t)offset, aSize))), CreateAndRejectBoolPromise); return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(syncLoopTarget, __func__, [this, &syncLoopTarget]( const BoolPromise::ResolveOrRejectValue& aValue) { MOZ_ASSERT(mWorkerRef); mWorkerRef->Private()->AssertIsOnWorkerThread(); mWorkerRef->Private()->StopSyncLoop( syncLoopTarget, aValue.IsResolve() ? NS_OK : aValue.RejectValue()); }); QM_TRY(MOZ_TO_RESULT(syncLoop.Run()), [&aError](const nsresult rv) { aError.Throw(rv); }); } uint64_t FileSystemSyncAccessHandle::GetSize(ErrorResult& aError) { if (!IsOpen()) { aError.ThrowInvalidStateError("SyncAccessHandle is closed"); return 0; } MOZ_ASSERT(mWorkerRef); AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aError](nsresult) { aError.ThrowInvalidStateError("Worker is shutting down"); return 0; }); // XXX Could we somehow pass the size to `StopSyncLoop` and then get it via // `QM_TRY_INSPECT(const auto& size, syncLoop.Run)` ? // Could we use Result, nsresult> for that ? int64_t size; InvokeAsync(mIOTaskQueue, __func__, [selfHolder = fs::TargetPtrHolder(this)]() { QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), CreateAndRejectSizePromise); nsCOMPtr fileMetadata = do_QueryInterface(selfHolder->mStream); MOZ_ASSERT(fileMetadata); QM_TRY_INSPECT( const auto& size, MOZ_TO_RESULT_INVOKE_MEMBER(fileMetadata, GetSize), CreateAndRejectSizePromise); LOG(("%p: GetSize %" PRIu64, selfHolder->mStream.get(), size)); return SizePromise::CreateAndResolve(size, __func__); }) ->Then(syncLoopTarget, __func__, [this, &syncLoopTarget, &size](const Int64Promise::ResolveOrRejectValue& aValue) { MOZ_ASSERT(mWorkerRef); mWorkerRef->Private()->AssertIsOnWorkerThread(); if (aValue.IsResolve()) { size = aValue.ResolveValue(); mWorkerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK); } else { mWorkerRef->Private()->StopSyncLoop(syncLoopTarget, aValue.RejectValue()); } }); QM_TRY(MOZ_TO_RESULT(syncLoop.Run()), [&aError](const nsresult rv) { aError.Throw(rv); return 0; }); return size; } void FileSystemSyncAccessHandle::Flush(ErrorResult& aError) { if (!IsOpen()) { aError.ThrowInvalidStateError("SyncAccessHandle is closed"); return; } MOZ_ASSERT(mWorkerRef); AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aError](nsresult) { aError.ThrowInvalidStateError("Worker is shutting down"); }); InvokeAsync(mIOTaskQueue, __func__, [selfHolder = fs::TargetPtrHolder(this)]() { QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), CreateAndRejectBoolPromise); LOG(("%p: Flush", selfHolder->mStream.get())); QM_TRY( MOZ_TO_RESULT(selfHolder->mStream->OutputStream()->Flush()), CreateAndRejectBoolPromise); return BoolPromise::CreateAndResolve(true, __func__); }) ->Then(syncLoopTarget, __func__, [this, &syncLoopTarget]( const BoolPromise::ResolveOrRejectValue& aValue) { MOZ_ASSERT(mWorkerRef); mWorkerRef->Private()->AssertIsOnWorkerThread(); mWorkerRef->Private()->StopSyncLoop( syncLoopTarget, aValue.IsResolve() ? NS_OK : aValue.RejectValue()); }); QM_TRY(MOZ_TO_RESULT(syncLoop.Run()), [&aError](const nsresult rv) { aError.Throw(rv); }); } void FileSystemSyncAccessHandle::Close() { if (!(IsOpen() || IsClosing())) { return; } MOZ_ASSERT(mWorkerRef); // Normally mWorkerRef can be used directly for stopping the sync loop, but // the async close is special because mWorkerRef is cleared as part of the // operation. That's why we need to use this extra strong ref to the // `StrongWorkerRef`. RefPtr workerRef = mWorkerRef; AutoSyncLoopHolder syncLoop(workerRef->Private(), Killing); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); MOZ_ASSERT(syncLoopTarget); InvokeAsync(syncLoopTarget, __func__, [self = RefPtr(this)]() { if (self->IsOpen()) { return self->BeginClose(); } return self->OnClose(); })->Then(syncLoopTarget, __func__, [&workerRef, &syncLoopTarget]() { MOZ_ASSERT(workerRef); workerRef->Private()->AssertIsOnWorkerThread(); workerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK); }); MOZ_ALWAYS_SUCCEEDS(syncLoop.Run()); } uint64_t FileSystemSyncAccessHandle::ReadOrWrite( const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer, const FileSystemReadWriteOptions& aOptions, const bool aRead, ErrorResult& aRv) { if (!IsOpen()) { aRv.ThrowInvalidStateError("SyncAccessHandle is closed"); return 0; } MOZ_ASSERT(mWorkerRef); auto throwAndReturn = [&aRv](const nsresult rv) { aRv.Throw(rv); return 0; }; const auto dataSpan = [&aBuffer]() { if (aBuffer.IsArrayBuffer()) { const ArrayBuffer& buffer = aBuffer.GetAsArrayBuffer(); buffer.ComputeState(); return Span{buffer.Data(), buffer.Length()}; } MOZ_ASSERT(aBuffer.IsArrayBufferView()); const ArrayBufferView& buffer = aBuffer.GetAsArrayBufferView(); buffer.ComputeState(); return Span{buffer.Data(), buffer.Length()}; }(); CheckedInt offset = 0; if (aOptions.mAt.WasPassed()) { // Handle seek before read ('at') offset = CheckedInt(aOptions.mAt.Value()); QM_TRY(MOZ_TO_RESULT(offset.isValid()), throwAndReturn); } AutoSyncLoopHolder syncLoop(mWorkerRef->Private(), Canceling); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); QM_TRY(MOZ_TO_RESULT(syncLoopTarget), [&aRv](nsresult) { aRv.ThrowInvalidStateError("Worker is shutting down"); return 0; }); uint64_t totalCount = 0; InvokeAsync( mIOTaskQueue, __func__, [selfHolder = fs::TargetPtrHolder(this), dataSpan, use_offset = aOptions.mAt.WasPassed(), offset, aRead, &totalCount]() { QM_TRY(MOZ_TO_RESULT(selfHolder->EnsureStream()), CreateAndRejectBoolPromise); if (use_offset) { LOG_VERBOSE(("%p: Seeking to %" PRIu64, selfHolder->mStream.get(), offset.value())); QM_TRY(MOZ_TO_RESULT(selfHolder->mStream->Seek( nsISeekableStream::NS_SEEK_SET, offset.value())), CreateAndRejectBoolPromise); } nsCOMPtr inputStream; nsCOMPtr outputStream; if (aRead) { LOG_VERBOSE(("%p: Reading %zu bytes", selfHolder->mStream.get(), dataSpan.Length())); inputStream = selfHolder->mStream->InputStream(); outputStream = FixedBufferOutputStream::Create(AsWritableChars(dataSpan)); } else { LOG_VERBOSE(("%p: Writing %zu bytes", selfHolder->mStream.get(), dataSpan.Length())); QM_TRY(MOZ_TO_RESULT(NS_NewByteInputStream( getter_AddRefs(inputStream), AsChars(dataSpan), NS_ASSIGNMENT_DEPEND)), CreateAndRejectBoolPromise); outputStream = selfHolder->mStream->OutputStream(); } auto promiseHolder = MakeUnique>(); RefPtr promise = promiseHolder->Ensure(__func__); QM_TRY(MOZ_TO_RESULT(fs::AsyncCopy( inputStream, outputStream, GetCurrentSerialEventTarget(), aRead ? NS_ASYNCCOPY_VIA_WRITESEGMENTS : NS_ASYNCCOPY_VIA_READSEGMENTS, /* aCloseSource */ !aRead, /* aCloseSink */ aRead, [&totalCount](uint32_t count) { totalCount += count; }, [promiseHolder = std::move(promiseHolder)](nsresult rv) { promiseHolder->ResolveIfExists(true, __func__); })), CreateAndRejectBoolPromise); return promise; }) ->Then(syncLoopTarget, __func__, [this, &syncLoopTarget]( const BoolPromise::ResolveOrRejectValue& aValue) { MOZ_ASSERT(mWorkerRef); mWorkerRef->Private()->AssertIsOnWorkerThread(); mWorkerRef->Private()->StopSyncLoop(syncLoopTarget, NS_OK); }); MOZ_ALWAYS_SUCCEEDS(syncLoop.Run()); return totalCount; } nsresult FileSystemSyncAccessHandle::EnsureStream() { if (!mStream) { QM_TRY_UNWRAP(mStream, DeserializeRandomAccessStream(mStreamParams), NS_ERROR_FAILURE); mozilla::ipc::RandomAccessStreamParams streamParams( std::move(mStreamParams)); } return NS_OK; } } // namespace mozilla::dom