diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/file/ipc | |
parent | Initial commit. (diff) | |
download | firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.tar.xz firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/file/ipc')
40 files changed, 4590 insertions, 0 deletions
diff --git a/dom/file/ipc/FileCreatorChild.cpp b/dom/file/ipc/FileCreatorChild.cpp new file mode 100644 index 0000000000..95ac19f17b --- /dev/null +++ b/dom/file/ipc/FileCreatorChild.cpp @@ -0,0 +1,59 @@ +/* -*- 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 "FileCreatorChild.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/IPCBlobUtils.h" + +namespace mozilla::dom { + +FileCreatorChild::FileCreatorChild() = default; + +FileCreatorChild::~FileCreatorChild() { MOZ_ASSERT(!mPromise); } + +void FileCreatorChild::SetPromise(Promise* aPromise) { + MOZ_ASSERT(aPromise); + MOZ_ASSERT(!mPromise); + + mPromise = aPromise; +} + +mozilla::ipc::IPCResult FileCreatorChild::Recv__delete__( + const FileCreationResult& aResult) { + MOZ_ASSERT(mPromise); + + RefPtr<Promise> promise; + promise.swap(mPromise); + + if (aResult.type() == FileCreationResult::TFileCreationErrorResult) { + promise->MaybeReject(aResult.get_FileCreationErrorResult().errorCode()); + return IPC_OK(); + } + + MOZ_ASSERT(aResult.type() == FileCreationResult::TFileCreationSuccessResult); + + RefPtr<dom::BlobImpl> impl = dom::IPCBlobUtils::Deserialize( + aResult.get_FileCreationSuccessResult().blob()); + + RefPtr<File> file = File::Create(promise->GetParentObject(), impl); + if (NS_WARN_IF(!file)) { + promise->MaybeReject(NS_ERROR_FAILURE); + return IPC_OK(); + } + + promise->MaybeResolve(file); + return IPC_OK(); +} + +void FileCreatorChild::ActorDestroy(ActorDestroyReason aWhy) { + if (mPromise) { + mPromise->MaybeReject(NS_ERROR_FAILURE); + mPromise = nullptr; + } +}; + +} // namespace mozilla::dom diff --git a/dom/file/ipc/FileCreatorChild.h b/dom/file/ipc/FileCreatorChild.h new file mode 100644 index 0000000000..eb5dc09d92 --- /dev/null +++ b/dom/file/ipc/FileCreatorChild.h @@ -0,0 +1,33 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_FileCreatorChild_h +#define mozilla_dom_FileCreatorChild_h + +#include "mozilla/dom/PFileCreatorChild.h" + +namespace mozilla::dom { + +class FileCreatorChild final : public mozilla::dom::PFileCreatorChild { + friend class mozilla::dom::PFileCreatorChild; + + public: + FileCreatorChild(); + ~FileCreatorChild() override; + + void SetPromise(Promise* aPromise); + + private: + mozilla::ipc::IPCResult Recv__delete__(const FileCreationResult& aResult); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr<Promise> mPromise; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileCreatorChild_h diff --git a/dom/file/ipc/FileCreatorParent.cpp b/dom/file/ipc/FileCreatorParent.cpp new file mode 100644 index 0000000000..77f8efd483 --- /dev/null +++ b/dom/file/ipc/FileCreatorParent.cpp @@ -0,0 +1,135 @@ +/* -*- 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 "FileCreatorParent.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/MultipartBlobImpl.h" +#include "nsIFile.h" + +namespace mozilla::dom { + +FileCreatorParent::FileCreatorParent() + : mBackgroundEventTarget(GetCurrentSerialEventTarget()), mIPCActive(true) {} + +FileCreatorParent::~FileCreatorParent() = default; + +mozilla::ipc::IPCResult FileCreatorParent::CreateAndShareFile( + const nsAString& aFullPath, const nsAString& aType, const nsAString& aName, + const Maybe<int64_t>& aLastModified, const bool& aExistenceCheck, + const bool& aIsFromNsIFile) { + RefPtr<dom::BlobImpl> blobImpl; + nsresult rv = + CreateBlobImpl(aFullPath, aType, aName, aLastModified.isSome(), + aLastModified.isSome() ? aLastModified.value() : 0, + aExistenceCheck, aIsFromNsIFile, getter_AddRefs(blobImpl)); + if (NS_WARN_IF(NS_FAILED(rv))) { + (void)Send__delete__(this, FileCreationErrorResult(rv)); + return IPC_OK(); + } + + MOZ_ASSERT(blobImpl); + + // FileBlobImpl is unable to return the correct type on this thread because + // nsIMIMEService is not thread-safe. We must exec the 'type' getter on + // main-thread before send the blob to the child actor. + + RefPtr<FileCreatorParent> self = this; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "FileCreatorParent::CreateAndShareFile", [self, blobImpl]() { + nsAutoString type; + blobImpl->GetType(type); + + self->mBackgroundEventTarget->Dispatch(NS_NewRunnableFunction( + "FileCreatorParent::CreateAndShareFile return", [self, blobImpl]() { + if (self->mIPCActive) { + IPCBlob ipcBlob; + nsresult rv = dom::IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + (void)Send__delete__(self, FileCreationErrorResult(rv)); + return; + } + + (void)Send__delete__(self, FileCreationSuccessResult(ipcBlob)); + } + })); + })); + + return IPC_OK(); +} + +void FileCreatorParent::ActorDestroy(ActorDestroyReason aWhy) { + mIPCActive = false; +} + +/* static */ +nsresult FileCreatorParent::CreateBlobImpl( + const nsAString& aPath, const nsAString& aType, const nsAString& aName, + bool aLastModifiedPassed, int64_t aLastModified, bool aExistenceCheck, + bool aIsFromNsIFile, BlobImpl** aBlobImpl) { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> file; + nsresult rv = NS_NewLocalFile(aPath, true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aExistenceCheck) { + if (!exists) { + return NS_ERROR_FILE_NOT_FOUND; + } + + bool isDir; + rv = file->IsDirectory(&isDir); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (isDir) { + return NS_ERROR_FILE_IS_DIRECTORY; + } + } + + RefPtr<FileBlobImpl> impl = new FileBlobImpl(file); + + // If the file doesn't exist, we cannot have its path, its size and so on. + // Let's set them now. + if (!exists) { + MOZ_ASSERT(!aExistenceCheck); + + impl->SetMozFullPath(aPath); + impl->SetLastModified(0); + impl->SetEmptySize(); + } + + if (!aName.IsEmpty()) { + impl->SetName(aName); + } + + if (!aType.IsEmpty()) { + impl->SetType(aType); + } + + if (aLastModifiedPassed) { + impl->SetLastModified(aLastModified); + } + + if (!aIsFromNsIFile) { + impl->SetMozFullPath(u""_ns); + } + + impl.forget(aBlobImpl); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/file/ipc/FileCreatorParent.h b/dom/file/ipc/FileCreatorParent.h new file mode 100644 index 0000000000..f43dbee886 --- /dev/null +++ b/dom/file/ipc/FileCreatorParent.h @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_FileCreatorParent_h +#define mozilla_dom_FileCreatorParent_h + +#include "mozilla/dom/PFileCreatorParent.h" + +class nsIFile; + +namespace mozilla::dom { + +class BlobImpl; + +class FileCreatorParent final : public mozilla::dom::PFileCreatorParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileCreatorParent) + + FileCreatorParent(); + + mozilla::ipc::IPCResult CreateAndShareFile( + const nsAString& aFullPath, const nsAString& aType, + const nsAString& aName, const Maybe<int64_t>& aLastModified, + const bool& aExistenceCheck, const bool& aIsFromNsIFile); + + private: + ~FileCreatorParent() override; + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsresult CreateBlobImpl(const nsAString& aPath, const nsAString& aType, + const nsAString& aName, bool aLastModifiedPassed, + int64_t aLastModified, bool aExistenceCheck, + bool aIsFromNsIFile, BlobImpl** aBlobImpl); + + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; + bool mIPCActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileCreatorParent_h diff --git a/dom/file/ipc/IPCBlob.ipdlh b/dom/file/ipc/IPCBlob.ipdlh new file mode 100644 index 0000000000..4db1c662bd --- /dev/null +++ b/dom/file/ipc/IPCBlob.ipdlh @@ -0,0 +1,56 @@ +/* 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 IPCStream; +include ProtocolTypes; + +using struct mozilla::void_t from "mozilla/ipc/IPCCore.h"; +[RefCounted] using class mozilla::RemoteLazyInputStream from "mozilla/RemoteLazyInputStream.h"; + +namespace mozilla { + +union RemoteLazyStream +{ + // Parent to Child: The child will receive a RemoteLazyInputStream. Nothing + // can be done with it except retrieving the size. + nullable RemoteLazyInputStream; + + // Child to Parent: Normal serialization. + IPCStream; +}; + +namespace dom { + +// This contains any extra bit for making a File out of a Blob. +// For more information about Blobs and IPC, please read the comments in +// IPCBlobUtils.h + +struct IPCFile +{ + nsString name; + int64_t lastModified; + nsString DOMPath; + nsString fullPath; + + // Useful for Entries API. + bool isDirectory; +}; + +struct IPCBlob +{ + nsString type; + uint64_t size; + nsString blobImplType; + + RemoteLazyStream inputStream; + + // Nothing is for Blob + IPCFile? file; + + // This ID is used only by indexedDB tests. + int64_t fileId; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/file/ipc/IPCBlobUtils.cpp b/dom/file/ipc/IPCBlobUtils.cpp new file mode 100644 index 0000000000..78409da529 --- /dev/null +++ b/dom/file/ipc/IPCBlobUtils.cpp @@ -0,0 +1,179 @@ +/* -*- 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 "IPCBlobUtils.h" +#include "RemoteLazyInputStream.h" +#include "RemoteLazyInputStreamChild.h" +#include "RemoteLazyInputStreamParent.h" +#include "mozilla/dom/IPCBlob.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "RemoteLazyInputStreamStorage.h" +#include "StreamBlobImpl.h" +#include "prtime.h" + +namespace mozilla::dom::IPCBlobUtils { + +already_AddRefed<BlobImpl> Deserialize(const IPCBlob& aIPCBlob) { + nsCOMPtr<nsIInputStream> inputStream; + + const RemoteLazyStream& stream = aIPCBlob.inputStream(); + switch (stream.type()) { + // Parent to child: when an nsIInputStream is sent from parent to child, the + // child receives a RemoteLazyInputStream actor. + case RemoteLazyStream::TRemoteLazyInputStream: { + inputStream = stream.get_RemoteLazyInputStream(); + break; + } + + // Child to Parent: when a blob is created on the content process send it's + // sent to the parent, we have an IPCStream object. + case RemoteLazyStream::TIPCStream: + MOZ_ASSERT(XRE_IsParentProcess()); + inputStream = DeserializeIPCStream(stream.get_IPCStream()); + break; + + default: + MOZ_CRASH("Unknown type."); + break; + } + + MOZ_ASSERT(inputStream); + + RefPtr<StreamBlobImpl> blobImpl; + + if (aIPCBlob.file().isNothing()) { + blobImpl = StreamBlobImpl::Create(inputStream.forget(), aIPCBlob.type(), + aIPCBlob.size(), aIPCBlob.blobImplType()); + } else { + const IPCFile& file = aIPCBlob.file().ref(); + blobImpl = StreamBlobImpl::Create(inputStream.forget(), file.name(), + aIPCBlob.type(), file.lastModified(), + aIPCBlob.size(), aIPCBlob.blobImplType()); + blobImpl->SetDOMPath(file.DOMPath()); + blobImpl->SetFullPath(file.fullPath()); + blobImpl->SetIsDirectory(file.isDirectory()); + } + + blobImpl->SetFileId(aIPCBlob.fileId()); + + return blobImpl.forget(); +} + +nsresult Serialize(BlobImpl* aBlobImpl, IPCBlob& aIPCBlob) { + MOZ_ASSERT(aBlobImpl); + + nsAutoString value; + aBlobImpl->GetType(value); + aIPCBlob.type() = value; + + aBlobImpl->GetBlobImplType(value); + aIPCBlob.blobImplType() = value; + + ErrorResult rv; + aIPCBlob.size() = aBlobImpl->GetSize(rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + if (!aBlobImpl->IsFile()) { + aIPCBlob.file() = Nothing(); + } else { + IPCFile file; + + aBlobImpl->GetName(value); + file.name() = value; + + file.lastModified() = aBlobImpl->GetLastModified(rv) * PR_USEC_PER_MSEC; + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + aBlobImpl->GetDOMPath(value); + file.DOMPath() = value; + + aBlobImpl->GetMozFullPathInternal(value, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + file.fullPath() = value; + + file.isDirectory() = aBlobImpl->IsDirectory(); + + aIPCBlob.file() = Some(file); + } + + aIPCBlob.fileId() = aBlobImpl->GetFileId(); + + nsCOMPtr<nsIInputStream> inputStream; + aBlobImpl->CreateInputStream(getter_AddRefs(inputStream), rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + if (XRE_IsParentProcess()) { + RefPtr<RemoteLazyInputStream> stream = + RemoteLazyInputStream::WrapStream(inputStream); + if (NS_WARN_IF(!stream)) { + return NS_ERROR_FAILURE; + } + + aIPCBlob.inputStream() = stream; + return NS_OK; + } + + mozilla::ipc::IPCStream stream; + if (!mozilla::ipc::SerializeIPCStream(inputStream.forget(), stream, + /* aAllowLazy */ true)) { + return NS_ERROR_FAILURE; + } + aIPCBlob.inputStream() = stream; + return NS_OK; +} + +} // namespace mozilla::dom::IPCBlobUtils + +namespace IPC { + +void ParamTraits<mozilla::dom::BlobImpl*>::Write( + IPC::MessageWriter* aWriter, mozilla::dom::BlobImpl* aParam) { + nsresult rv; + mozilla::dom::IPCBlob ipcblob; + if (aParam) { + rv = mozilla::dom::IPCBlobUtils::Serialize(aParam, ipcblob); + } + if (!aParam || NS_WARN_IF(NS_FAILED(rv))) { + WriteParam(aWriter, false); + } else { + WriteParam(aWriter, true); + WriteParam(aWriter, ipcblob); + } +} + +bool ParamTraits<mozilla::dom::BlobImpl*>::Read( + IPC::MessageReader* aReader, RefPtr<mozilla::dom::BlobImpl>* aResult) { + *aResult = nullptr; + + bool notnull = false; + if (!ReadParam(aReader, ¬null)) { + return false; + } + if (notnull) { + mozilla::dom::IPCBlob ipcblob; + if (!ReadParam(aReader, &ipcblob)) { + return false; + } + *aResult = mozilla::dom::IPCBlobUtils::Deserialize(ipcblob); + } + return true; +} + +} // namespace IPC diff --git a/dom/file/ipc/IPCBlobUtils.h b/dom/file/ipc/IPCBlobUtils.h new file mode 100644 index 0000000000..17fce3195a --- /dev/null +++ b/dom/file/ipc/IPCBlobUtils.h @@ -0,0 +1,268 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_IPCBlobUtils_h +#define mozilla_dom_IPCBlobUtils_h + +#include "mozilla/RefPtr.h" +#include "mozilla/dom/File.h" +#include "mozilla/ipc/IPDLParamTraits.h" + +/* + * Blobs and IPC + * ~~~~~~~~~~~~~ + * + * Simplifying, DOM Blob objects are chunks of data with a content type and a + * size. DOM Files are Blobs with a name. They are are used in many APIs and + * they can be cloned and sent cross threads and cross processes. + * + * If we see Blobs from a platform point of view, the main (and often, the only) + * interesting part is how to retrieve data from it. This is done via + * nsIInputStream and, except for a couple of important details, this stream is + * used in the parent process. + * + * For this reason, when we consider the serialization of a blob via IPC + * messages, the biggest effort is put in how to manage the nsInputStream + * correctly. To serialize, we use the IPCBlob data struct: basically, the blob + * properties (size, type, name if it's a file) and the nsIInputStream. + * + * Before talking about the nsIInputStream it's important to say that we have + * different kinds of Blobs, based on the different kinds of sources. A non + * exaustive list is: + * - a memory buffer: MemoryBlobImpl + * - a string: StringBlobImpl + * - a real OS file: FileBlobImpl + * - a generic nsIInputStream: StreamBlobImpl + * - an empty blob: EmptyBlobImpl + * - more blobs combined together: MultipartBlobImpl + * Each one of these implementations has a custom ::CreateInputStream method. + * So, basically, each one has a different kind of nsIInputStream (nsFileStream, + * nsIStringInputStream, SlicedInputStream, and so on). + * + * Another important point to keep in mind is that a Blob can be created on the + * content process (for example: |new Blob([123])|) or it can be created on the + * parent process and sent to content (a FilePicker creates Blobs and it runs on + * the parent process). + * + * DocumentLoadListener uses blobs to serialize the POST data back to the + * content process (for insertion into session history). This lets it correctly + * handle OS files by reference, and avoid copying the underlying buffer data + * unless it is read. This can hopefully be removed once SessionHistory is + * handled in the parent process. + * + * Child to Parent Blob Serialization + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * When a document creates a blob, this can be sent, for different reasons to + * the parent process. For instance it can be sent as part of a FormData, or it + * can be converted to a BlobURL and broadcasted to any other existing + * processes. + * + * When this happens, we use the IPCStream data struct for the serialization + * of the nsIInputStream. This means that, if the stream is fully serializable + * and its size is lower than 1Mb, we are able to recreate the stream completely + * on the parent side. This happens, basically with any kind of child-to-parent + * stream except for huge memory streams. In this case we end up using + * DataPipe. See more information in IPCStreamUtils.h. + * + * In order to populate IPCStream correctly, we use SerializeIPCStream as + * documented in IPCStreamUtils.h. + * + * Parent to Child Blob Serialization + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This scenario is common when we talk about Blobs pointing to real files: + * HTMLInputElement (type=file), or Entries API, DataTransfer and so on. But we + * also have this scenario when a content process creates a Blob and it + * broadcasts it because of a BlobURL or because BroadcastChannel API is used. + * + * The approach here is this: normally, the content process doesn't really read + * data from the blob nsIInputStream. The content process needs to have the + * nsIInputStream and be able to send it back to the parent process when the + * "real" work needs to be done. This is true except for 2 usecases: FileReader + * API and BlobURL usage. So, if we ignore these 2, normally, the parent sends a + * blob nsIInputStream to a content process, and then, it will receive it back + * in order to do some networking, or whatever. + * + * For this reason, IPCBlobUtils uses a particular protocol for serializing + * nsIInputStream parent to child: PRemoteLazyInputStream. This protocol keeps + * the original nsIInputStream alive on the parent side, and gives its size and + * a UUID to the child side. The child side creates a RemoteLazyInputStream and + * that is incapsulated into a StreamBlobImpl. + * + * The UUID is useful when the content process sends the same nsIInputStream + * back to the parent process because, the only information it has to share is + * the UUID. Each nsIInputStream sent via PRemoteLazyInputStream, is registered + * into the RemoteLazyInputStreamStorage. + * + * On the content process side, RemoteLazyInputStream is a special inputStream: + * the only reliable methods are: + * - nsIInputStream.available() - the size is shared by PRemoteLazyInputStream + * actor. + * - nsIIPCSerializableInputStream.serialize() - we can give back this stream to + * the parent because we know its UUID. + * - nsICloneableInputStream.cloneable() and nsICloneableInputStream.clone() - + * this stream can be cloned. We just need to have a reference of the + * PRemoteLazyInputStream actor and its UUID. + * - nsIAsyncInputStream.asyncWait() - see next section. + * + * Any other method (read, readSegment and so on) will fail if asyncWait() is + * not previously called (see the next section). Basically, this inputStream + * cannot be used synchronously for any 'real' reading operation. + * + * When the parent receives the serialization of a RemoteLazyInputStream, it is + * able to retrieve the correct nsIInputStream using the UUID and + * RemoteLazyInputStreamStorage. + * + * Parent to Child Streams, FileReader and BlobURL + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * The FileReader and BlobURL scenarios are described here. + * + * When content process needs to read data from a Blob sent from the parent + * process, it must do it asynchronously using RemoteLazyInputStream as a + * nsIAsyncInputStream stream. This happens calling + * RemoteLazyInputStream.asyncWait(). At that point, the child actor will send a + * StreamNeeded() IPC message to the parent side. When this is received, the + * parent retrieves the 'real' stream from RemoteLazyInputStreamStorage using + * the UUID, it will serialize the 'real' stream, and it will send it to the + * child side. + * + * When the 'real' stream is received (RecvStreamReady()), the asyncWait + * callback will be executed and, from that moment, any RemoteLazyInputStream + * method will be forwarded to the 'real' stream ones. This means that the + * reading will be available. + * + * RemoteLazyInputStream Thread + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * RemoteLazyInputStreamChild actor can be created in any thread (sort of) and + * their top-level IPDL protocol is PBackground. These actors are wrapped by 1 + * or more RemoteLazyInputStream objects in order to expose nsIInputStream + * interface and be thread-safe. + * + * But IPDL actors are not thread-safe and any SendFoo() method must be executed + * on the owning thread. This means that this thread must be kept alive for the + * life-time of the RemoteLazyInputStream. + * + * In doing this, there are 2 main issues: + * a. if a remote Blob is created on a worker (because of a + * BroadcastChannel/MessagePort for instance) and it sent to the main-thread + * via PostMessage(), we have to keep that worker alive. + * b. if the remote Blob is created on the main-thread, any SendFoo() has to be + * executed on the main-thread. This is true also when the inputStream is + * used on another thread (note that nsIInputStream could do I/O and usually + * they are used on special I/O threads). + * + * In order to avoid this, RemoteLazyInputStreamChild are 'migrated' to a + * DOM-File thread. This is done in this way: + * + * 1. If RemoteLazyInputStreamChild actor is not already owned by DOM-File + * thread, it calls Send__delete__ in order to inform the parent side that we + * don't need this IPC channel on the current thread. + * 2. A new RemoteLazyInputStreamChild is created. RemoteLazyInputStreamThread + * is used to assign this actor to the DOM-File thread. + * RemoteLazyInputStreamThread::GetOrCreate() creates the DOM-File thread if + * it doesn't exist yet. Pending operations and RemoteLazyInputStreams are + * moved onto the new actor. + * 3. RemoteLazyInputStreamParent::Recv__delete__ is called on the parent side + * and the parent actor is deleted. Doing this we don't remove the UUID from + * RemoteLazyInputStreamStorage. + * 4. The RemoteLazyInputStream constructor is sent with the new + * RemoteLazyInputStreamChild actor, with the DOM-File thread's PBackground + * as its manager. + * 5. When the new RemoteLazyInputStreamParent actor is created, it will receive + * the same UUID of the previous parent actor. The nsIInputStream will be + * retrieved from RemoteLazyInputStreamStorage. + * 6. In order to avoid leaks, RemoteLazyInputStreamStorage will monitor child + * processes and in case one of them dies, it will release the + * nsIInputStream objects belonging to that process. + * + * If any API wants to retrieve a 'real inputStream when the migration is in + * progress, that operation is stored in a pending queue and processed at the + * end of the migration. + * + * IPCBlob and nsIAsyncInputStream + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * RemoteLazyInputStream is always async. If the remote inputStream is not + * async, RemoteLazyInputStream will create a pipe stream around it in order to + * be consistently async. + * + * Slicing IPCBlob + * ~~~~~~~~~~~~~~~ + * + * Normally, slicing a blob consists of the creation of a new Blob, with a + * SlicedInputStream() wrapping a clone of the original inputStream. But this + * approach is extremely inefficient with IPCBlob, because it could be that we + * wrap the pipe stream and not the remote inputStream (See the previous section + * of this documentation). If we end up doing so, also if the remote + * inputStream is seekable, the pipe will not be, and in order to reach the + * starting point, SlicedInputStream will do consecutive read()s. + * + * This problem is fixed implmenting nsICloneableWithRange in + * RemoteLazyInputStream and using cloneWithRange() when a StreamBlobImpl is + * sliced. When the remote stream is received, it will be sliced directly. + * + * If we want to represent the hierarchy of the InputStream classes, instead + * of having: |SlicedInputStream(RemoteLazyInputStream(Async + * Pipe(RemoteStream)))|, we have: |RemoteLazyInputStream(Async + * Pipe(SlicedInputStream(RemoteStream)))|. + * + * When RemoteLazyInputStream is serialized and sent to the parent process, + * start and range are sent too and SlicedInputStream is used in the parent side + * as well. + * + * Socket Process + * ~~~~~~~~~~~~~~ + * + * The socket process is a separate process used to do networking operations. + * When a website sends a blob as the body of a POST/PUT request, we need to + * send the corresponding RemoteLazyInputStream to the socket process. + * + * This is the only serialization of RemoteLazyInputStream from parent to child + * process and it works _only_ for the socket process. Do not expose this + * serialization to PContent or PBackground or any other top-level IPDL protocol + * without a DOM File peer review! + * + * The main difference between Socket Process is that DOM-File thread is not + * used. Here is a list of reasons: + * - DOM-File moves the ownership of the RemoteLazyInputStream actors to + * PBackground, but in the Socket Process we don't have PBackground (yet?) + * - Socket Process is a stable process with a simple life-time configuration: + * we can keep the actors on the main-thread because no Workers are involved. + */ + +namespace mozilla::dom { + +class IPCBlob; + +namespace IPCBlobUtils { + +already_AddRefed<BlobImpl> Deserialize(const IPCBlob& aIPCBlob); + +nsresult Serialize(BlobImpl* aBlobImpl, IPCBlob& aIPCBlob); + +} // namespace IPCBlobUtils +} // namespace mozilla::dom + +namespace IPC { + +// ParamTraits implementation for BlobImpl. N.B: If the original BlobImpl cannot +// be successfully serialized, a warning will be produced and a nullptr will be +// sent over the wire. When Read()-ing a BlobImpl, +// __always make sure to handle null!__ +template <> +struct ParamTraits<mozilla::dom::BlobImpl*> { + static void Write(IPC::MessageWriter* aWriter, + mozilla::dom::BlobImpl* aParam); + static bool Read(IPC::MessageReader* aReader, + RefPtr<mozilla::dom::BlobImpl>* aResult); +}; + +} // namespace IPC + +#endif // mozilla_dom_IPCBlobUtils_h diff --git a/dom/file/ipc/PFileCreator.ipdl b/dom/file/ipc/PFileCreator.ipdl new file mode 100644 index 0000000000..d690820906 --- /dev/null +++ b/dom/file/ipc/PFileCreator.ipdl @@ -0,0 +1,38 @@ +/* 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 protocol PBackground; + +include IPCBlob; + +namespace mozilla { +namespace dom { + +struct FileCreationSuccessResult +{ + IPCBlob blob; +}; + +struct FileCreationErrorResult +{ + nsresult errorCode; +}; + +union FileCreationResult +{ + FileCreationSuccessResult; + FileCreationErrorResult; +}; + +[ManualDealloc] +protocol PFileCreator +{ + manager PBackground; + +child: + async __delete__(FileCreationResult aResult); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/file/ipc/PRemoteLazyInputStream.ipdl b/dom/file/ipc/PRemoteLazyInputStream.ipdl new file mode 100644 index 0000000000..82fab20cdb --- /dev/null +++ b/dom/file/ipc/PRemoteLazyInputStream.ipdl @@ -0,0 +1,21 @@ +/* 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 IPCStream; + +namespace mozilla { + +protocol PRemoteLazyInputStream +{ +parent: + async Clone(Endpoint<PRemoteLazyInputStreamParent> aCloneEndpoint); + + async StreamNeeded(uint64_t aStart, uint64_t aLength) returns (IPCStream? stream); + + async LengthNeeded() returns (int64_t length); + + async Goodbye(); +}; + +} // namespace mozilla diff --git a/dom/file/ipc/PTemporaryIPCBlob.ipdl b/dom/file/ipc/PTemporaryIPCBlob.ipdl new file mode 100644 index 0000000000..2645d6fd50 --- /dev/null +++ b/dom/file/ipc/PTemporaryIPCBlob.ipdl @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include IPCBlob; + +namespace mozilla { +namespace dom { + +union IPCBlobOrError +{ + IPCBlob; + nsresult; +}; + +[ManualDealloc] +protocol PTemporaryIPCBlob +{ + manager PBackground; + + // When this actor is created on the child side, the parent will send + // immediatelly back a FileDescriptor or a __delete__ in case of error. + // When the FileDescriptor is received, the child has to call + // OperationDone(). When OperationDone() is received on the parent side, the + // parent actor will send a __delete__. + +child: + async FileDesc(FileDescriptor aFD); + async __delete__(IPCBlobOrError aBlobOrError); + +parent: + async OperationFailed(); + + async OperationDone(nsCString aContentType, FileDescriptor aFD); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStream.cpp b/dom/file/ipc/RemoteLazyInputStream.cpp new file mode 100644 index 0000000000..936438df13 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStream.cpp @@ -0,0 +1,1458 @@ +/* -*- 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 "RemoteLazyInputStream.h" +#include "RemoteLazyInputStreamChild.h" +#include "RemoteLazyInputStreamParent.h" +#include "chrome/common/ipc_message_utils.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Logging.h" +#include "mozilla/PRemoteLazyInputStream.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/MessageChannel.h" +#include "mozilla/ipc/ProtocolMessageUtils.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/SlicedInputStream.h" +#include "mozilla/NonBlockingAsyncInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsID.h" +#include "nsIInputStream.h" +#include "nsIPipe.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "RemoteLazyInputStreamStorage.h" +#include "RemoteLazyInputStreamThread.h" + +namespace mozilla { + +mozilla::LazyLogModule gRemoteLazyStreamLog("RemoteLazyStream"); + +namespace { + +class InputStreamCallbackRunnable final : public DiscardableRunnable { + public: + // Note that the execution can be synchronous in case the event target is + // null. + static void Execute(already_AddRefed<nsIInputStreamCallback> aCallback, + already_AddRefed<nsIEventTarget> aEventTarget, + RemoteLazyInputStream* aStream) { + RefPtr<InputStreamCallbackRunnable> runnable = + new InputStreamCallbackRunnable(std::move(aCallback), aStream); + + nsCOMPtr<nsIEventTarget> target = std::move(aEventTarget); + if (target) { + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } else { + runnable->Run(); + } + } + + NS_IMETHOD + Run() override { + mCallback->OnInputStreamReady(mStream); + mCallback = nullptr; + mStream = nullptr; + return NS_OK; + } + + private: + InputStreamCallbackRunnable( + already_AddRefed<nsIInputStreamCallback> aCallback, + RemoteLazyInputStream* aStream) + : DiscardableRunnable("dom::InputStreamCallbackRunnable"), + mCallback(std::move(aCallback)), + mStream(aStream) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + RefPtr<nsIInputStreamCallback> mCallback; + RefPtr<RemoteLazyInputStream> mStream; +}; + +class FileMetadataCallbackRunnable final : public DiscardableRunnable { + public: + static void Execute(nsIFileMetadataCallback* aCallback, + nsIEventTarget* aEventTarget, + RemoteLazyInputStream* aStream) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aEventTarget); + + RefPtr<FileMetadataCallbackRunnable> runnable = + new FileMetadataCallbackRunnable(aCallback, aStream); + + nsCOMPtr<nsIEventTarget> target = aEventTarget; + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + + NS_IMETHOD + Run() override { + mCallback->OnFileMetadataReady(mStream); + mCallback = nullptr; + mStream = nullptr; + return NS_OK; + } + + private: + FileMetadataCallbackRunnable(nsIFileMetadataCallback* aCallback, + RemoteLazyInputStream* aStream) + : DiscardableRunnable("dom::FileMetadataCallbackRunnable"), + mCallback(aCallback), + mStream(aStream) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + nsCOMPtr<nsIFileMetadataCallback> mCallback; + RefPtr<RemoteLazyInputStream> mStream; +}; + +} // namespace + +NS_IMPL_ADDREF(RemoteLazyInputStream); +NS_IMPL_RELEASE(RemoteLazyInputStream); + +NS_INTERFACE_MAP_BEGIN(RemoteLazyInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsICloneableInputStream) + NS_INTERFACE_MAP_ENTRY(nsICloneableInputStreamWithRange) + NS_INTERFACE_MAP_ENTRY(nsIIPCSerializableInputStream) + NS_INTERFACE_MAP_ENTRY(nsIFileMetadata) + NS_INTERFACE_MAP_ENTRY(nsIAsyncFileMetadata) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength) + NS_INTERFACE_MAP_ENTRY(mozIRemoteLazyInputStream) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) +NS_INTERFACE_MAP_END + +RemoteLazyInputStream::RemoteLazyInputStream(RemoteLazyInputStreamChild* aActor, + uint64_t aStart, uint64_t aLength) + : mStart(aStart), mLength(aLength), mState(eInit), mActor(aActor) { + MOZ_ASSERT(aActor); + + mActor->StreamCreated(); + + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + nsCOMPtr<nsIInputStream> stream; + storage->GetStream(mActor->StreamID(), mStart, mLength, + getter_AddRefs(stream)); + if (stream) { + mState = eRunning; + mInnerStream = stream; + } + } +} + +RemoteLazyInputStream::RemoteLazyInputStream(nsIInputStream* aStream) + : mStart(0), mLength(UINT64_MAX), mState(eRunning), mInnerStream(aStream) {} + +static already_AddRefed<RemoteLazyInputStreamChild> BindChildActor( + nsID aId, mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> aEndpoint) { + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return nullptr; + } + auto actor = MakeRefPtr<RemoteLazyInputStreamChild>(aId); + thread->Dispatch( + NS_NewRunnableFunction("RemoteLazyInputStream::BindChildActor", + [actor, childEp = std::move(aEndpoint)]() mutable { + bool ok = childEp.Bind(actor); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Binding child actor for %s (%p): %s", + nsIDToCString(actor->StreamID()).get(), + actor.get(), ok ? "OK" : "ERROR")); + })); + + return actor.forget(); +} + +already_AddRefed<RemoteLazyInputStream> RemoteLazyInputStream::WrapStream( + nsIInputStream* aInputStream) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (nsCOMPtr<mozIRemoteLazyInputStream> lazyStream = + do_QueryInterface(aInputStream)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Returning already-wrapped stream")); + return lazyStream.forget().downcast<RemoteLazyInputStream>(); + } + + // If we have a stream and are in the parent process, create a new actor pair + // and transfer ownership of the stream into storage. + auto streamStorage = RemoteLazyInputStreamStorage::Get(); + if (NS_WARN_IF(streamStorage.isErr())) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Cannot wrap with no storage!")); + return nullptr; + } + + nsID id = nsID::GenerateUUID(); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Wrapping stream %p as %s", aInputStream, nsIDToCString(id).get())); + streamStorage.inspect()->AddStream(aInputStream, id); + + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp; + MOZ_ALWAYS_SUCCEEDS( + PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp)); + + // Bind the actor on our background thread. + streamStorage.inspect()->TaskQueue()->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStreamParent::Bind", + [parentEp = std::move(parentEp), id]() mutable { + auto actor = MakeRefPtr<RemoteLazyInputStreamParent>(id); + bool ok = parentEp.Bind(actor); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Binding parent actor for %s (%p): %s", + nsIDToCString(id).get(), actor.get(), ok ? "OK" : "ERROR")); + })); + + RefPtr<RemoteLazyInputStreamChild> actor = + BindChildActor(id, std::move(childEp)); + + if (!actor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Wrapping stream failed as we are probably late in shutdown!")); + return do_AddRef(new RemoteLazyInputStream()); + } + + return do_AddRef(new RemoteLazyInputStream(actor)); +} + +NS_IMETHODIMP RemoteLazyInputStream::TakeInternalStream( + nsIInputStream** aStream) { + RefPtr<RemoteLazyInputStreamChild> actor; + { + MutexAutoLock lock(mMutex); + if (mState == eInit || mState == ePending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + if (mInputStreamCallback) { + MOZ_ASSERT_UNREACHABLE( + "Do not call TakeInternalStream after calling AsyncWait"); + return NS_ERROR_UNEXPECTED; + } + + // Take the inner stream and return it, then close ourselves. + if (mInnerStream) { + mInnerStream.forget(aStream); + } else if (mAsyncInnerStream) { + mAsyncInnerStream.forget(aStream); + } + mState = eClosed; + actor = mActor.forget(); + } + if (actor) { + actor->StreamConsumed(); + } + return NS_OK; +} + +NS_IMETHODIMP RemoteLazyInputStream::GetInternalStreamID(nsID& aID) { + MutexAutoLock lock(mMutex); + if (!mActor) { + return NS_ERROR_NOT_AVAILABLE; + } + + aID = mActor->StreamID(); + return NS_OK; +} + +RemoteLazyInputStream::~RemoteLazyInputStream() { Close(); } + +nsCString RemoteLazyInputStream::Describe() { + const char* state = "?"; + switch (mState) { + case eInit: + state = "i"; + break; + case ePending: + state = "p"; + break; + case eRunning: + state = "r"; + break; + case eClosed: + state = "c"; + break; + } + return nsPrintfCString( + "[%p, %s, %s, %p%s, %s%s|%s%s]", this, state, + mActor ? nsIDToCString(mActor->StreamID()).get() : "<no actor>", + mInnerStream ? mInnerStream.get() : mAsyncInnerStream.get(), + mAsyncInnerStream ? "(A)" : "", mInputStreamCallback ? "I" : "", + mInputStreamCallbackEventTarget ? "+" : "", + mFileMetadataCallback ? "F" : "", + mFileMetadataCallbackEventTarget ? "+" : ""); +} + +// nsIInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::Available(uint64_t* aLength) { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + // We don't have a remoteStream yet: let's return 0. + if (mState == eInit || mState == ePending) { + *aLength = 0; + return NS_OK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + return stream->Available(aLength); +} + +NS_IMETHODIMP +RemoteLazyInputStream::StreamStatus() { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + // We don't have a remoteStream yet: let's return 0. + if (mState == eInit || mState == ePending) { + return NS_OK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + return stream->StreamStatus(); +} + +NS_IMETHODIMP +RemoteLazyInputStream::Read(char* aBuffer, uint32_t aCount, + uint32_t* aReadCount) { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Read(%u) %s", aCount, Describe().get())); + + // Read is not available is we don't have a remoteStream. + if (mState == eInit || mState == ePending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + nsresult rv = stream->Read(aBuffer, aCount, aReadCount); + if (NS_FAILED(rv)) { + return rv; + } + + // If some data has been read, we mark the stream as consumed. + if (*aReadCount > 0) { + MarkConsumed(); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Read %u/%u bytes", *aReadCount, aCount)); + + return NS_OK; +} + +NS_IMETHODIMP +RemoteLazyInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* aResult) { + nsCOMPtr<nsIAsyncInputStream> stream; + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("ReadSegments(%u) %s", aCount, Describe().get())); + + // ReadSegments is not available is we don't have a remoteStream. + if (mState == eInit || mState == ePending) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + MOZ_ASSERT(mInnerStream || mAsyncInnerStream); + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("EnsureAsyncRemoteStream failed! %s %s", + mozilla::GetStaticErrorName(rv), Describe().get())); + return rv; + } + + stream = mAsyncInnerStream; + } + + MOZ_ASSERT(stream); + nsresult rv = stream->ReadSegments(aWriter, aClosure, aCount, aResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // If some data has been read, we mark the stream as consumed. + if (*aResult != 0) { + MarkConsumed(); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("ReadSegments %u/%u bytes", *aResult, aCount)); + + return NS_OK; +} + +void RemoteLazyInputStream::MarkConsumed() { + RefPtr<RemoteLazyInputStreamChild> actor; + { + MutexAutoLock lock(mMutex); + if (mActor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("MarkConsumed %s", Describe().get())); + } + + actor = mActor.forget(); + } + if (actor) { + actor->StreamConsumed(); + } +} + +NS_IMETHODIMP +RemoteLazyInputStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = true; + return NS_OK; +} + +NS_IMETHODIMP +RemoteLazyInputStream::Close() { + RefPtr<RemoteLazyInputStreamChild> actor; + + nsCOMPtr<nsIAsyncInputStream> asyncInnerStream; + nsCOMPtr<nsIInputStream> innerStream; + + RefPtr<nsIInputStreamCallback> inputStreamCallback; + nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget; + + { + MutexAutoLock lock(mMutex); + if (mState == eClosed) { + return NS_OK; + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Close %s", Describe().get())); + + actor = mActor.forget(); + + asyncInnerStream = mAsyncInnerStream.forget(); + innerStream = mInnerStream.forget(); + + // TODO(Bug 1737783): Notify to the mFileMetadataCallback that this + // lazy input stream has been closed. + mFileMetadataCallback = nullptr; + mFileMetadataCallbackEventTarget = nullptr; + + inputStreamCallback = mInputStreamCallback.forget(); + inputStreamCallbackEventTarget = mInputStreamCallbackEventTarget.forget(); + + mState = eClosed; + } + + if (actor) { + actor->StreamConsumed(); + } + + if (inputStreamCallback) { + InputStreamCallbackRunnable::Execute( + inputStreamCallback.forget(), inputStreamCallbackEventTarget.forget(), + this); + } + + if (asyncInnerStream) { + asyncInnerStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); + } + + if (innerStream) { + innerStream->Close(); + } + + return NS_OK; +} + +// nsICloneableInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::GetCloneable(bool* aCloneable) { + *aCloneable = true; + return NS_OK; +} + +NS_IMETHODIMP +RemoteLazyInputStream::Clone(nsIInputStream** aResult) { + return CloneWithRange(0, UINT64_MAX, aResult); +} + +// nsICloneableInputStreamWithRange interface + +NS_IMETHODIMP +RemoteLazyInputStream::CloneWithRange(uint64_t aStart, uint64_t aLength, + nsIInputStream** aResult) { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("CloneWithRange %" PRIu64 " %" PRIu64 " %s", aStart, aLength, + Describe().get())); + + nsresult rv; + + RefPtr<RemoteLazyInputStream> stream; + if (mState == eClosed) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning closed stream")); + stream = new RemoteLazyInputStream(); + stream.forget(aResult); + return NS_OK; + } + + uint64_t start = 0; + uint64_t length = 0; + auto maxLength = CheckedUint64(mLength) - aStart; + if (maxLength.isValid()) { + start = mStart + aStart; + length = std::min(maxLength.value(), aLength); + } + + // If the slice would be empty, wrap an empty input stream and return it. + if (length == 0) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Creating empty stream")); + + nsCOMPtr<nsIInputStream> emptyStream; + rv = NS_NewCStringInputStream(getter_AddRefs(emptyStream), ""_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = new RemoteLazyInputStream(emptyStream); + stream.forget(aResult); + return NS_OK; + } + + // If we still have a connection to our actor, that means we haven't read any + // data yet, and can clone + slice by building a new stream backed by the same + // actor. + if (mActor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Cloning stream with actor")); + + stream = new RemoteLazyInputStream(mActor, start, length); + stream.forget(aResult); + return NS_OK; + } + + // We no longer have our actor, either because we were constructed without + // one, or we've already begun reading. Perform the clone locally on our inner + // input stream. + + nsCOMPtr<nsIInputStream> innerStream = mInnerStream; + if (mAsyncInnerStream) { + innerStream = mAsyncInnerStream; + } + + nsCOMPtr<nsICloneableInputStream> cloneable = do_QueryInterface(innerStream); + if (!cloneable || !cloneable->GetCloneable()) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Cloning non-cloneable stream - copying to pipe")); + + // If our internal stream isn't cloneable, to perform a clone we'll need to + // copy into a pipe and replace our internal stream. + nsCOMPtr<nsIAsyncInputStream> pipeIn; + nsCOMPtr<nsIAsyncOutputStream> pipeOut; + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true); + + RefPtr<RemoteLazyInputStreamThread> thread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + mAsyncInnerStream = pipeIn; + mInnerStream = nullptr; + + // If we have a callback pending, we need to re-call AsyncWait on the inner + // stream. This should not re-enter us immediately, as `pipeIn` hasn't been + // sent any data yet, but we may be called again as soon as `NS_AsyncCopy` + // has begun copying. + if (mInputStreamCallback) { + mAsyncInnerStream->AsyncWait(this, mInputStreamCallbackFlags, + mInputStreamCallbackRequestedCount, + mInputStreamCallbackEventTarget); + } + + rv = NS_AsyncCopy(innerStream, pipeOut, thread, + NS_ASYNCCOPY_VIA_WRITESEGMENTS); + if (NS_WARN_IF(NS_FAILED(rv))) { + // The copy failed, revert the changes we did and restore our previous + // inner stream. + mAsyncInnerStream = nullptr; + mInnerStream = innerStream; + return rv; + } + + cloneable = do_QueryInterface(mAsyncInnerStream); + } + + MOZ_ASSERT(cloneable && cloneable->GetCloneable()); + + // Check if we can clone more efficiently with a range. + if (length < UINT64_MAX) { + if (nsCOMPtr<nsICloneableInputStreamWithRange> cloneableWithRange = + do_QueryInterface(cloneable)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Cloning with range")); + nsCOMPtr<nsIInputStream> cloned; + rv = cloneableWithRange->CloneWithRange(start, length, + getter_AddRefs(cloned)); + if (NS_FAILED(rv)) { + return rv; + } + + stream = new RemoteLazyInputStream(cloned); + stream.forget(aResult); + return NS_OK; + } + } + + // Directly clone our inner stream, and then slice it if needed. + nsCOMPtr<nsIInputStream> cloned; + rv = cloneable->Clone(getter_AddRefs(cloned)); + if (NS_FAILED(rv)) { + return rv; + } + + if (length < UINT64_MAX) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Slicing stream with %" PRIu64 " %" PRIu64, start, length)); + cloned = new SlicedInputStream(cloned.forget(), start, length); + } + + stream = new RemoteLazyInputStream(cloned); + stream.forget(aResult); + return NS_OK; +} + +// nsIAsyncInputStream interface + +NS_IMETHODIMP +RemoteLazyInputStream::CloseWithStatus(nsresult aStatus) { return Close(); } + +NS_IMETHODIMP +RemoteLazyInputStream::AsyncWait(nsIInputStreamCallback* aCallback, + uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + // Ensure we always have an event target for AsyncWait callbacks, so that + // calls to `AsyncWait` cannot reenter us with `OnInputStreamReady`. + nsCOMPtr<nsIEventTarget> eventTarget = aEventTarget; + if (aCallback && !eventTarget) { + eventTarget = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!eventTarget)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + } + + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncWait(%p, %u, %u, %p) %s", aCallback, aFlags, aRequestedCount, + aEventTarget, Describe().get())); + + // See RemoteLazyInputStream.h for more information about this state + // machine. + + nsCOMPtr<nsIAsyncInputStream> stream; + switch (mState) { + // First call, we need to retrieve the stream from the parent actor. + case eInit: + MOZ_ASSERT(mActor); + + mInputStreamCallback = aCallback; + mInputStreamCallbackEventTarget = eventTarget; + mInputStreamCallbackFlags = aFlags; + mInputStreamCallbackRequestedCount = aRequestedCount; + mState = ePending; + + StreamNeeded(); + return NS_OK; + + // We are still waiting for the remote inputStream + case ePending: { + if (NS_WARN_IF(mInputStreamCallback && aCallback && + mInputStreamCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + + mInputStreamCallback = aCallback; + mInputStreamCallbackEventTarget = eventTarget; + mInputStreamCallbackFlags = aFlags; + mInputStreamCallbackRequestedCount = aRequestedCount; + return NS_OK; + } + + // We have the remote inputStream, let's check if we can execute the + // callback. + case eRunning: { + if (NS_WARN_IF(mInputStreamCallback && aCallback && + mInputStreamCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = EnsureAsyncRemoteStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInputStreamCallback = aCallback; + mInputStreamCallbackEventTarget = eventTarget; + mInputStreamCallbackFlags = aFlags; + mInputStreamCallbackRequestedCount = aRequestedCount; + + stream = mAsyncInnerStream; + break; + } + + case eClosed: + [[fallthrough]]; + default: + MOZ_ASSERT(mState == eClosed); + if (NS_WARN_IF(mInputStreamCallback && aCallback && + mInputStreamCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + break; + } + + if (stream) { + return stream->AsyncWait(aCallback ? this : nullptr, aFlags, + aRequestedCount, eventTarget); + } + } + + if (aCallback) { + // if stream is nullptr here, that probably means the stream has + // been closed and the callback can be executed immediately + InputStreamCallbackRunnable::Execute(do_AddRef(aCallback), + do_AddRef(eventTarget), this); + } + return NS_OK; +} + +void RemoteLazyInputStream::StreamNeeded() { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("StreamNeeded %s", Describe().get())); + + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return; + } + thread->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStream::StreamNeeded", + [self = RefPtr{this}, actor = mActor, start = mStart, length = mLength] { + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Debug, + ("Sending StreamNeeded(%" PRIu64 " %" PRIu64 ") %s %d", start, + length, nsIDToCString(actor->StreamID()).get(), actor->CanSend())); + + actor->SendStreamNeeded( + start, length, + [self](const Maybe<mozilla::ipc::IPCStream>& aStream) { + // Try to deserialize the stream from our remote, and close our + // stream if it fails. + nsCOMPtr<nsIInputStream> stream = + mozilla::ipc::DeserializeIPCStream(aStream); + if (NS_WARN_IF(!stream)) { + NS_WARNING("Failed to deserialize IPC stream"); + self->Close(); + } + + // Lock our mutex to update the inner stream, and collect any + // callbacks which we need to invoke. + MutexAutoLock lock(self->mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("ResolveStreamNeeded(%p) %s", stream.get(), + self->Describe().get())); + + if (self->mState == ePending) { + self->mInnerStream = stream.forget(); + self->mState = eRunning; + + // Notify any listeners that we've now acquired the underlying + // stream, so file metadata information will be available. + nsCOMPtr<nsIFileMetadataCallback> fileMetadataCallback = + self->mFileMetadataCallback.forget(); + nsCOMPtr<nsIEventTarget> fileMetadataCallbackEventTarget = + self->mFileMetadataCallbackEventTarget.forget(); + if (fileMetadataCallback) { + FileMetadataCallbackRunnable::Execute( + fileMetadataCallback, fileMetadataCallbackEventTarget, + self); + } + + // **NOTE** we can re-enter this class here **NOTE** + // If we already have an input stream callback, attempt to + // register ourselves with AsyncWait on the underlying stream. + if (self->mInputStreamCallback) { + if (NS_FAILED(self->EnsureAsyncRemoteStream()) || + NS_FAILED(self->mAsyncInnerStream->AsyncWait( + self, self->mInputStreamCallbackFlags, + self->mInputStreamCallbackRequestedCount, + self->mInputStreamCallbackEventTarget))) { + InputStreamCallbackRunnable::Execute( + self->mInputStreamCallback.forget(), + self->mInputStreamCallbackEventTarget.forget(), self); + } + } + } + + if (stream) { + NS_WARNING("Failed to save stream, closing it"); + stream->Close(); + } + }, + [self](mozilla::ipc::ResponseRejectReason) { + NS_WARNING("SendStreamNeeded rejected"); + self->Close(); + }); + })); +} + +// nsIInputStreamCallback + +NS_IMETHODIMP +RemoteLazyInputStream::OnInputStreamReady(nsIAsyncInputStream* aStream) { + RefPtr<nsIInputStreamCallback> callback; + nsCOMPtr<nsIEventTarget> callbackEventTarget; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("OnInputStreamReady %s", Describe().get())); + + // We have been closed in the meantime. + if (mState == eClosed) { + return NS_OK; + } + + // We got a callback from the wrong stream, likely due to a `CloneWithRange` + // call while we were waiting. Ignore this callback. + if (mAsyncInnerStream != aStream) { + return NS_OK; + } + + MOZ_ASSERT(mState == eRunning); + + // The callback has been canceled in the meantime. + if (!mInputStreamCallback) { + return NS_OK; + } + + callback.swap(mInputStreamCallback); + callbackEventTarget.swap(mInputStreamCallbackEventTarget); + } + + // This must be the last operation because the execution of the callback can + // be synchronous. + MOZ_ASSERT(callback); + InputStreamCallbackRunnable::Execute(callback.forget(), + callbackEventTarget.forget(), this); + return NS_OK; +} + +// nsIIPCSerializableInputStream + +void RemoteLazyInputStream::SerializedComplexity(uint32_t aMaxSize, + uint32_t* aSizeUsed, + uint32_t* aNewPipes, + uint32_t* aTransferables) { + *aTransferables = 1; +} + +void RemoteLazyInputStream::Serialize(mozilla::ipc::InputStreamParams& aParams, + uint32_t aMaxSize, uint32_t* aSizeUsed) { + *aSizeUsed = 0; + aParams = mozilla::ipc::RemoteLazyInputStreamParams(this); +} + +bool RemoteLazyInputStream::Deserialize( + const mozilla::ipc::InputStreamParams& aParams) { + MOZ_CRASH("This should never be called."); + return false; +} + +// nsIAsyncFileMetadata + +NS_IMETHODIMP +RemoteLazyInputStream::AsyncFileMetadataWait(nsIFileMetadataCallback* aCallback, + nsIEventTarget* aEventTarget) { + MOZ_ASSERT(!!aCallback == !!aEventTarget); + + // If we have the callback, we must have the event target. + if (NS_WARN_IF(!!aCallback != !!aEventTarget)) { + return NS_ERROR_FAILURE; + } + + // See RemoteLazyInputStream.h for more information about this state + // machine. + + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("AsyncFileMetadataWait(%p, %p) %s", aCallback, aEventTarget, + Describe().get())); + + switch (mState) { + // First call, we need to retrieve the stream from the parent actor. + case eInit: + MOZ_ASSERT(mActor); + + mFileMetadataCallback = aCallback; + mFileMetadataCallbackEventTarget = aEventTarget; + mState = ePending; + + StreamNeeded(); + return NS_OK; + + // We are still waiting for the remote inputStream + case ePending: + if (mFileMetadataCallback && aCallback) { + return NS_ERROR_FAILURE; + } + + mFileMetadataCallback = aCallback; + mFileMetadataCallbackEventTarget = aEventTarget; + return NS_OK; + + // We have the remote inputStream, let's check if we can execute the + // callback. + case eRunning: + break; + + // Stream is closed. + default: + MOZ_ASSERT(mState == eClosed); + return NS_BASE_STREAM_CLOSED; + } + + MOZ_ASSERT(mState == eRunning); + } + + FileMetadataCallbackRunnable::Execute(aCallback, aEventTarget, this); + return NS_OK; +} + +// nsIFileMetadata + +NS_IMETHODIMP +RemoteLazyInputStream::GetSize(int64_t* aRetval) { + nsCOMPtr<nsIFileMetadata> fileMetadata; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("GetSize %s", Describe().get())); + + fileMetadata = do_QueryInterface(mInnerStream); + if (!fileMetadata) { + return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE; + } + } + + return fileMetadata->GetSize(aRetval); +} + +NS_IMETHODIMP +RemoteLazyInputStream::GetLastModified(int64_t* aRetval) { + nsCOMPtr<nsIFileMetadata> fileMetadata; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("GetLastModified %s", Describe().get())); + + fileMetadata = do_QueryInterface(mInnerStream); + if (!fileMetadata) { + return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE; + } + } + + return fileMetadata->GetLastModified(aRetval); +} + +NS_IMETHODIMP +RemoteLazyInputStream::GetFileDescriptor(PRFileDesc** aRetval) { + nsCOMPtr<nsIFileMetadata> fileMetadata; + { + MutexAutoLock lock(mMutex); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("GetFileDescriptor %s", Describe().get())); + + fileMetadata = do_QueryInterface(mInnerStream); + if (!fileMetadata) { + return mState == eClosed ? NS_BASE_STREAM_CLOSED : NS_ERROR_FAILURE; + } + } + + return fileMetadata->GetFileDescriptor(aRetval); +} + +nsresult RemoteLazyInputStream::EnsureAsyncRemoteStream() { + // We already have an async remote stream. + if (mAsyncInnerStream) { + return NS_OK; + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("EnsureAsyncRemoteStream %s", Describe().get())); + + if (NS_WARN_IF(!mInnerStream)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIInputStream> stream = mInnerStream; + + // Check if the stream is blocking, if it is, we want to make it non-blocking + // using a pipe. + bool nonBlocking = false; + nsresult rv = stream->IsNonBlocking(&nonBlocking); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We don't return NS_ERROR_NOT_IMPLEMENTED from ReadSegments, + // so it's possible that callers are expecting us to succeed in the future. + // We need to make sure the stream we return here supports ReadSegments, + // so wrap if in a buffered stream if necessary. + // + // We only need to do this if we won't be wrapping the stream in a pipe, which + // will add buffering anyway. + if (nonBlocking && !NS_InputStreamIsBuffered(stream)) { + nsCOMPtr<nsIInputStream> bufferedStream; + nsresult rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + stream.forget(), 4096); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + stream = bufferedStream; + } + + nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(stream); + + // If non-blocking and non-async, let's use NonBlockingAsyncInputStream. + if (nonBlocking && !asyncStream) { + rv = NonBlockingAsyncInputStream::Create(stream.forget(), + getter_AddRefs(asyncStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(asyncStream); + } + + if (!asyncStream) { + // Let's make the stream async using the DOMFile thread. + nsCOMPtr<nsIAsyncInputStream> pipeIn; + nsCOMPtr<nsIAsyncOutputStream> pipeOut; + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(pipeOut), true, true); + + RefPtr<RemoteLazyInputStreamThread> thread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + rv = NS_AsyncCopy(stream, pipeOut, thread, NS_ASYNCCOPY_VIA_WRITESEGMENTS); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + asyncStream = pipeIn; + } + + MOZ_ASSERT(asyncStream); + mAsyncInnerStream = asyncStream; + mInnerStream = nullptr; + + return NS_OK; +} + +// nsIInputStreamLength + +NS_IMETHODIMP +RemoteLazyInputStream::Length(int64_t* aLength) { + MutexAutoLock lock(mMutex); + + if (mState == eClosed) { + return NS_BASE_STREAM_CLOSED; + } + + if (!mActor) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_BASE_STREAM_WOULD_BLOCK; +} + +namespace { + +class InputStreamLengthCallbackRunnable final : public DiscardableRunnable { + public: + static void Execute(nsIInputStreamLengthCallback* aCallback, + nsIEventTarget* aEventTarget, + RemoteLazyInputStream* aStream, int64_t aLength) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aEventTarget); + + RefPtr<InputStreamLengthCallbackRunnable> runnable = + new InputStreamLengthCallbackRunnable(aCallback, aStream, aLength); + + nsCOMPtr<nsIEventTarget> target = aEventTarget; + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + + NS_IMETHOD + Run() override { + mCallback->OnInputStreamLengthReady(mStream, mLength); + mCallback = nullptr; + mStream = nullptr; + return NS_OK; + } + + private: + InputStreamLengthCallbackRunnable(nsIInputStreamLengthCallback* aCallback, + RemoteLazyInputStream* aStream, + int64_t aLength) + : DiscardableRunnable("dom::InputStreamLengthCallbackRunnable"), + mCallback(aCallback), + mStream(aStream), + mLength(aLength) { + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mStream); + } + + nsCOMPtr<nsIInputStreamLengthCallback> mCallback; + RefPtr<RemoteLazyInputStream> mStream; + const int64_t mLength; +}; + +} // namespace + +// nsIAsyncInputStreamLength + +NS_IMETHODIMP +RemoteLazyInputStream::AsyncLengthWait(nsIInputStreamLengthCallback* aCallback, + nsIEventTarget* aEventTarget) { + // If we have the callback, we must have the event target. + if (NS_WARN_IF(!!aCallback != !!aEventTarget)) { + return NS_ERROR_FAILURE; + } + + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncLengthWait(%p, %p) %s", aCallback, aEventTarget, + Describe().get())); + + if (mActor) { + if (aCallback) { + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!thread)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + thread->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStream::AsyncLengthWait", + [self = RefPtr{this}, actor = mActor, + callback = nsCOMPtr{aCallback}, + eventTarget = nsCOMPtr{aEventTarget}] { + actor->SendLengthNeeded( + [self, callback, eventTarget](int64_t aLength) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncLengthWait resolve %" PRId64, aLength)); + int64_t length = -1; + if (aLength > 0) { + uint64_t sourceLength = + aLength - std::min<uint64_t>(aLength, self->mStart); + length = int64_t( + std::min<uint64_t>(sourceLength, self->mLength)); + } + InputStreamLengthCallbackRunnable::Execute( + callback, eventTarget, self, length); + }, + [self, callback, + eventTarget](mozilla::ipc::ResponseRejectReason) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("AsyncLengthWait reject")); + InputStreamLengthCallbackRunnable::Execute( + callback, eventTarget, self, -1); + }); + })); + } + + return NS_OK; + } + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("AsyncLengthWait immediate")); + + // If execution has reached here, it means the stream is either closed or + // consumed, and therefore the callback can be executed immediately + InputStreamLengthCallbackRunnable::Execute(aCallback, aEventTarget, this, -1); + return NS_OK; +} + +void RemoteLazyInputStream::IPCWrite(IPC::MessageWriter* aWriter) { + // If we have an actor still, serialize efficiently by cloning our actor to + // maintain a reference to the parent side. + RefPtr<RemoteLazyInputStreamChild> actor; + + nsCOMPtr<nsIInputStream> innerStream; + + RefPtr<nsIInputStreamCallback> inputStreamCallback; + nsCOMPtr<nsIEventTarget> inputStreamCallbackEventTarget; + + { + MutexAutoLock lock(mMutex); + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Serialize %s", Describe().get())); + + actor = mActor.forget(); + + if (mAsyncInnerStream) { + MOZ_ASSERT(!mInnerStream); + innerStream = mAsyncInnerStream.forget(); + } else { + innerStream = mInnerStream.forget(); + } + + // TODO(Bug 1737783): Notify to the mFileMetadataCallback that this + // lazy input stream has been closed. + mFileMetadataCallback = nullptr; + mFileMetadataCallbackEventTarget = nullptr; + + inputStreamCallback = mInputStreamCallback.forget(); + inputStreamCallbackEventTarget = mInputStreamCallbackEventTarget.forget(); + + mState = eClosed; + } + + if (inputStreamCallback) { + InputStreamCallbackRunnable::Execute( + inputStreamCallback.forget(), inputStreamCallbackEventTarget.forget(), + this); + } + + bool closed = !actor && !innerStream; + IPC::WriteParam(aWriter, closed); + if (closed) { + return; + } + + // If we still have a connection to our remote actor, create a clone endpoint + // for it and tell it that the stream has been consumed. The clone of the + // connection can be transferred to another process. + if (actor) { + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Debug, + ("Serializing as actor: %s", nsIDToCString(actor->StreamID()).get())); + // Create a clone of the actor, and then tell it that this stream is no + // longer referencing it. + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp; + MOZ_ALWAYS_SUCCEEDS( + PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp)); + + auto* thread = RemoteLazyInputStreamThread::GetOrCreate(); + if (thread) { + thread->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStreamChild::SendClone", + [actor, parentEp = std::move(parentEp)]() mutable { + bool ok = actor->SendClone(std::move(parentEp)); + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Verbose, + ("SendClone for %s: %s", nsIDToCString(actor->StreamID()).get(), + ok ? "OK" : "ERR")); + })); + + } // else we are shutting down xpcom threads. + + // NOTE: Call `StreamConsumed` after dispatching the `SendClone` runnable, + // as this method may dispatch a runnable to `RemoteLazyInputStreamThread` + // to call `SendGoodbye`, which needs to happen after `SendClone`. + actor->StreamConsumed(); + + IPC::WriteParam(aWriter, actor->StreamID()); + IPC::WriteParam(aWriter, mStart); + IPC::WriteParam(aWriter, mLength); + IPC::WriteParam(aWriter, std::move(childEp)); + + if (innerStream) { + innerStream->Close(); + } + return; + } + + // If we have a stream and are in the parent process, create a new actor pair + // and transfer ownership of the stream into storage. + auto streamStorage = RemoteLazyInputStreamStorage::Get(); + if (streamStorage.isOk()) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsID id = nsID::GenerateUUID(); + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Serializing as new stream: %s", nsIDToCString(id).get())); + + streamStorage.inspect()->AddStream(innerStream, id); + + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent> parentEp; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> childEp; + MOZ_ALWAYS_SUCCEEDS( + PRemoteLazyInputStream::CreateEndpoints(&parentEp, &childEp)); + + // Bind the actor on our background thread. + streamStorage.inspect()->TaskQueue()->Dispatch(NS_NewRunnableFunction( + "RemoteLazyInputStreamParent::Bind", + [parentEp = std::move(parentEp), id]() mutable { + auto stream = MakeRefPtr<RemoteLazyInputStreamParent>(id); + parentEp.Bind(stream); + })); + + IPC::WriteParam(aWriter, id); + IPC::WriteParam(aWriter, 0); + IPC::WriteParam(aWriter, UINT64_MAX); + IPC::WriteParam(aWriter, std::move(childEp)); + return; + } + + MOZ_CRASH("Cannot serialize new RemoteLazyInputStream from this process"); +} + +already_AddRefed<RemoteLazyInputStream> RemoteLazyInputStream::IPCRead( + IPC::MessageReader* aReader) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, ("Deserialize")); + + bool closed; + if (NS_WARN_IF(!IPC::ReadParam(aReader, &closed))) { + return nullptr; + } + if (closed) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Deserialize closed stream")); + return do_AddRef(new RemoteLazyInputStream()); + } + + nsID id{}; + uint64_t start; + uint64_t length; + mozilla::ipc::Endpoint<PRemoteLazyInputStreamChild> endpoint; + if (NS_WARN_IF(!IPC::ReadParam(aReader, &id)) || + NS_WARN_IF(!IPC::ReadParam(aReader, &start)) || + NS_WARN_IF(!IPC::ReadParam(aReader, &length)) || + NS_WARN_IF(!IPC::ReadParam(aReader, &endpoint))) { + return nullptr; + } + + if (!endpoint.IsValid()) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Deserialize failed due to invalid endpoint!")); + return do_AddRef(new RemoteLazyInputStream()); + } + + RefPtr<RemoteLazyInputStreamChild> actor = + BindChildActor(id, std::move(endpoint)); + + if (!actor) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Deserialize failed as we are probably late in shutdown!")); + return do_AddRef(new RemoteLazyInputStream()); + } + + return do_AddRef(new RemoteLazyInputStream(actor, start, length)); +} + +} // namespace mozilla + +void IPC::ParamTraits<mozilla::RemoteLazyInputStream*>::Write( + IPC::MessageWriter* aWriter, mozilla::RemoteLazyInputStream* aParam) { + bool nonNull = !!aParam; + IPC::WriteParam(aWriter, nonNull); + if (aParam) { + aParam->IPCWrite(aWriter); + } +} + +bool IPC::ParamTraits<mozilla::RemoteLazyInputStream*>::Read( + IPC::MessageReader* aReader, + RefPtr<mozilla::RemoteLazyInputStream>* aResult) { + bool nonNull = false; + if (!IPC::ReadParam(aReader, &nonNull)) { + return false; + } + if (!nonNull) { + *aResult = nullptr; + return true; + } + *aResult = mozilla::RemoteLazyInputStream::IPCRead(aReader); + return *aResult; +} diff --git a/dom/file/ipc/RemoteLazyInputStream.h b/dom/file/ipc/RemoteLazyInputStream.h new file mode 100644 index 0000000000..08bb168e27 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStream.h @@ -0,0 +1,155 @@ +/* -*- 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/. */ + +#ifndef mozilla_RemoteLazyInputStream_h +#define mozilla_RemoteLazyInputStream_h + +#include "chrome/common/ipc_message_utils.h" +#include "mozilla/Mutex.h" +#include "mozIRemoteLazyInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsICloneableInputStream.h" +#include "nsIFileStreams.h" +#include "nsIIPCSerializableInputStream.h" +#include "nsIInputStreamLength.h" +#include "nsCOMPtr.h" + +namespace mozilla { + +class RemoteLazyInputStreamChild; + +class RemoteLazyInputStream final : public nsIAsyncInputStream, + public nsIInputStreamCallback, + public nsICloneableInputStreamWithRange, + public nsIIPCSerializableInputStream, + public nsIAsyncFileMetadata, + public nsIInputStreamLength, + public nsIAsyncInputStreamLength, + public mozIRemoteLazyInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSICLONEABLEINPUTSTREAM + NS_DECL_NSICLONEABLEINPUTSTREAMWITHRANGE + NS_DECL_NSIIPCSERIALIZABLEINPUTSTREAM + NS_DECL_NSIFILEMETADATA + NS_DECL_NSIASYNCFILEMETADATA + NS_DECL_NSIINPUTSTREAMLENGTH + NS_DECL_NSIASYNCINPUTSTREAMLENGTH + + // Create a new lazy RemoteLazyInputStream, and move the provided aInputStream + // into storage as referenced by it. May only be called in processes with + // RemoteLazyInputStreamStorage. + static already_AddRefed<RemoteLazyInputStream> WrapStream( + nsIInputStream* aInputStream); + + // mozIRemoteLazyInputStream + NS_IMETHOD TakeInternalStream(nsIInputStream** aStream) override; + NS_IMETHOD GetInternalStreamID(nsID& aID) override; + + private: + friend struct IPC::ParamTraits<mozilla::RemoteLazyInputStream*>; + + // Constructor for an already-closed RemoteLazyInputStream. + RemoteLazyInputStream() = default; + + explicit RemoteLazyInputStream(RemoteLazyInputStreamChild* aActor, + uint64_t aStart = 0, + uint64_t aLength = UINT64_MAX); + + explicit RemoteLazyInputStream(nsIInputStream* aStream); + + ~RemoteLazyInputStream(); + + void StreamNeeded() MOZ_REQUIRES(mMutex); + + // Upon receiving the stream from our actor, we will not wrap it into an async + // stream until needed. This allows callers to get access to the underlying + // potentially-sync stream using `TakeInternalStream` before reading. + nsresult EnsureAsyncRemoteStream() MOZ_REQUIRES(mMutex); + + // Note that data has been read from our input stream, and disconnect from our + // remote actor. + void MarkConsumed(); + + void IPCWrite(IPC::MessageWriter* aWriter); + static already_AddRefed<RemoteLazyInputStream> IPCRead( + IPC::MessageReader* aReader); + + // Helper method to generate a description of a stream for use in loggging. + nsCString Describe() MOZ_REQUIRES(mMutex); + + // Start and length of the slice to apply on this RemoteLazyInputStream when + // fetching the underlying stream with `SendStreamNeeded`. + const uint64_t mStart = 0; + const uint64_t mLength = UINT64_MAX; + + // Any non-const member of this class is protected by mutex because it is + // touched on multiple threads. + Mutex mMutex{"RemoteLazyInputStream::mMutex"}; + + // This is the list of possible states. + enum { + // The initial state. Only ::Available() can be used without receiving an + // error. The available size is known by the actor. + eInit, + + // AsyncWait() has been called for the first time. SendStreamNeeded() has + // been called and we are waiting for the 'real' inputStream. + ePending, + + // When the child receives the stream from the parent, we move to this + // state. The received stream is stored in mInnerStream. From now on, any + // method call will be forwared to mInnerStream or mAsyncInnerStream. + eRunning, + + // If Close() or CloseWithStatus() is called, we move to this state. + // mInnerStream is released and any method will return + // NS_BASE_STREAM_CLOSED. + eClosed, + } mState MOZ_GUARDED_BY(mMutex) = eClosed; + + // The actor which will be used to provide the underlying stream or length + // information when needed, as well as to efficiently allow transferring the + // stream over IPC. + // + // The connection to our actor will be cleared once the stream has been closed + // or has started reading, at which point this stream will be serialized and + // cloned as-if it was the underlying stream. + RefPtr<RemoteLazyInputStreamChild> mActor MOZ_GUARDED_BY(mMutex); + + nsCOMPtr<nsIInputStream> mInnerStream MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIAsyncInputStream> mAsyncInnerStream MOZ_GUARDED_BY(mMutex); + + // These 2 values are set only if mState is ePending or eRunning. + // RefPtr is used instead of nsCOMPtr to avoid invoking QueryInterface when + // assigning in debug builds, as `mInputStreamCallback` may not be threadsafe. + RefPtr<nsIInputStreamCallback> mInputStreamCallback MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIEventTarget> mInputStreamCallbackEventTarget + MOZ_GUARDED_BY(mMutex); + uint32_t mInputStreamCallbackFlags MOZ_GUARDED_BY(mMutex) = 0; + uint32_t mInputStreamCallbackRequestedCount MOZ_GUARDED_BY(mMutex) = 0; + + // These 2 values are set only if mState is ePending. + nsCOMPtr<nsIFileMetadataCallback> mFileMetadataCallback + MOZ_GUARDED_BY(mMutex); + nsCOMPtr<nsIEventTarget> mFileMetadataCallbackEventTarget + MOZ_GUARDED_BY(mMutex); +}; + +} // namespace mozilla + +template <> +struct IPC::ParamTraits<mozilla::RemoteLazyInputStream*> { + static void Write(IPC::MessageWriter* aWriter, + mozilla::RemoteLazyInputStream* aParam); + static bool Read(IPC::MessageReader* aReader, + RefPtr<mozilla::RemoteLazyInputStream>* aResult); +}; + +#endif // mozilla_RemoteLazyInputStream_h diff --git a/dom/file/ipc/RemoteLazyInputStreamChild.cpp b/dom/file/ipc/RemoteLazyInputStreamChild.cpp new file mode 100644 index 0000000000..f03aa47aad --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamChild.cpp @@ -0,0 +1,55 @@ +/* -*- 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 "RemoteLazyInputStreamChild.h" +#include "RemoteLazyInputStreamThread.h" + +namespace mozilla { + +extern mozilla::LazyLogModule gRemoteLazyStreamLog; + +RemoteLazyInputStreamChild::RemoteLazyInputStreamChild(const nsID& aID) + : mID(aID) {} + +RemoteLazyInputStreamChild::~RemoteLazyInputStreamChild() = default; + +void RemoteLazyInputStreamChild::StreamCreated() { + size_t count = ++mStreamCount; + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Child::StreamCreated %s = %zu", nsIDToCString(mID).get(), count)); +} + +void RemoteLazyInputStreamChild::StreamConsumed() { + size_t count = --mStreamCount; + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Child::StreamConsumed %s = %zu", nsIDToCString(mID).get(), count)); + + // When the count reaches zero, close the underlying actor. + if (count == 0) { + auto* t = RemoteLazyInputStreamThread::Get(); + if (t) { + t->Dispatch( + NS_NewRunnableFunction("RemoteLazyInputStreamChild::StreamConsumed", + [self = RefPtr{this}]() { + if (self->CanSend()) { + self->SendGoodbye(); + } + })); + } // else the xpcom thread shutdown has already started. + } +} + +void RemoteLazyInputStreamChild::ActorDestroy(ActorDestroyReason aReason) { + if (mStreamCount != 0) { + NS_WARNING( + nsPrintfCString("RemoteLazyInputStreamChild disconnected unexpectedly " + "(%zu streams remaining)! %p %s", + size_t(mStreamCount), this, nsIDToCString(mID).get()) + .get()); + } +} + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamChild.h b/dom/file/ipc/RemoteLazyInputStreamChild.h new file mode 100644 index 0000000000..c95ed9f058 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamChild.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef mozilla_RemoteLazyInputStreamChild_h +#define mozilla_RemoteLazyInputStreamChild_h + +#include "mozilla/PRemoteLazyInputStreamChild.h" + +namespace mozilla { + +class RemoteLazyInputStream; + +class RemoteLazyInputStreamChild final : public PRemoteLazyInputStreamChild { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteLazyInputStreamChild, final) + + explicit RemoteLazyInputStreamChild(const nsID& aID); + + const nsID& StreamID() const { return mID; } + + // Manage the count of streams registered on this actor. When the count + // reaches 0 the connection to our remote process will be closed. + void StreamCreated(); + void StreamConsumed(); + + void ActorDestroy(ActorDestroyReason aReason) override; + + private: + ~RemoteLazyInputStreamChild() override; + + const nsID mID; + + std::atomic<size_t> mStreamCount{0}; +}; + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamChild_h diff --git a/dom/file/ipc/RemoteLazyInputStreamParent.cpp b/dom/file/ipc/RemoteLazyInputStreamParent.cpp new file mode 100644 index 0000000000..d92c529546 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamParent.cpp @@ -0,0 +1,123 @@ +/* -*- 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 "RemoteLazyInputStreamParent.h" +#include "RemoteLazyInputStreamStorage.h" +#include "mozilla/InputStreamLengthHelper.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/InputStreamParams.h" +#include "mozilla/ipc/ProtocolUtils.h" +#include "nsStreamUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsNetCID.h" + +namespace mozilla { + +extern mozilla::LazyLogModule gRemoteLazyStreamLog; + +RemoteLazyInputStreamParent::RemoteLazyInputStreamParent(const nsID& aID) + : mID(aID) { + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->ActorCreated(mID); + } +} + +void RemoteLazyInputStreamParent::ActorDestroy( + IProtocol::ActorDestroyReason aReason) { + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->ActorDestroyed(mID); + } +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvClone( + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent>&& aCloneEndpoint) { + if (!aCloneEndpoint.IsValid()) { + return IPC_FAIL(this, "Unexpected invalid endpoint in RecvClone"); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Debug, + ("Parent::RecvClone %s", nsIDToCString(mID).get())); + + auto* newActor = new RemoteLazyInputStreamParent(mID); + aCloneEndpoint.Bind(newActor); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvStreamNeeded( + uint64_t aStart, uint64_t aLength, StreamNeededResolver&& aResolver) { + nsCOMPtr<nsIInputStream> stream; + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->GetStream(mID, aStart, aLength, getter_AddRefs(stream)); + } + + if (!stream) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Parent::RecvStreamNeeded not available! %s", + nsIDToCString(mID).get())); + aResolver(Nothing()); + return IPC_OK(); + } + + Maybe<IPCStream> ipcStream; + if (NS_WARN_IF(!SerializeIPCStream(stream.forget(), ipcStream, + /* aAllowLazy */ false))) { + return IPC_FAIL(this, "IPCStream serialization failed!"); + } + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvStreamNeeded resolve %s", nsIDToCString(mID).get())); + aResolver(ipcStream); + return IPC_OK(); +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvLengthNeeded( + LengthNeededResolver&& aResolver) { + nsCOMPtr<nsIInputStream> stream; + auto storage = RemoteLazyInputStreamStorage::Get().unwrapOr(nullptr); + if (storage) { + storage->GetStream(mID, 0, UINT64_MAX, getter_AddRefs(stream)); + } + + if (!stream) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Warning, + ("Parent::RecvLengthNeeded not available! %s", + nsIDToCString(mID).get())); + aResolver(-1); + return IPC_OK(); + } + + int64_t length = -1; + if (InputStreamLengthHelper::GetSyncLength(stream, &length)) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvLengthNeeded sync resolve %" PRId64 "! %s", length, + nsIDToCString(mID).get())); + aResolver(length); + return IPC_OK(); + } + + InputStreamLengthHelper::GetAsyncLength( + stream, [aResolver, id = mID](int64_t aLength) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvLengthNeeded async resolve %" PRId64 "! %s", + aLength, nsIDToCString(id).get())); + aResolver(aLength); + }); + return IPC_OK(); +} + +mozilla::ipc::IPCResult RemoteLazyInputStreamParent::RecvGoodbye() { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Parent::RecvGoodbye! %s", nsIDToCString(mID).get())); + Close(); + return IPC_OK(); +} + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamParent.h b/dom/file/ipc/RemoteLazyInputStreamParent.h new file mode 100644 index 0000000000..cb5b1d4285 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamParent.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_RemoteLazyInputStreamParent_h +#define mozilla_RemoteLazyInputStreamParent_h + +#include "mozilla/PRemoteLazyInputStreamParent.h" + +class nsIInputStream; + +namespace mozilla { + +class RemoteLazyInputStreamParent final : public PRemoteLazyInputStreamParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteLazyInputStreamParent, final) + + explicit RemoteLazyInputStreamParent(const nsID& aID); + + const nsID& ID() const { return mID; } + + mozilla::ipc::IPCResult RecvClone( + mozilla::ipc::Endpoint<PRemoteLazyInputStreamParent>&& aCloneEndpoint); + + mozilla::ipc::IPCResult RecvStreamNeeded(uint64_t aStart, uint64_t aLength, + StreamNeededResolver&& aResolver); + + mozilla::ipc::IPCResult RecvLengthNeeded(LengthNeededResolver&& aResolver); + + mozilla::ipc::IPCResult RecvGoodbye(); + + void ActorDestroy(IProtocol::ActorDestroyReason aReason) override; + + private: + ~RemoteLazyInputStreamParent() override = default; + + const nsID mID; +}; + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamParent_h diff --git a/dom/file/ipc/RemoteLazyInputStreamStorage.cpp b/dom/file/ipc/RemoteLazyInputStreamStorage.cpp new file mode 100644 index 0000000000..8ce2c22657 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamStorage.cpp @@ -0,0 +1,243 @@ +/* -*- 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 "mozilla/SlicedInputStream.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "nsIPropertyBag2.h" +#include "nsStreamUtils.h" +#include "RemoteLazyInputStreamParent.h" +#include "RemoteLazyInputStreamStorage.h" + +namespace mozilla { + +using namespace hal; + +extern mozilla::LazyLogModule gRemoteLazyStreamLog; + +namespace { +StaticMutex gMutex; +StaticRefPtr<RemoteLazyInputStreamStorage> gStorage; +} // namespace + +NS_INTERFACE_MAP_BEGIN(RemoteLazyInputStreamStorage) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(RemoteLazyInputStreamStorage) +NS_IMPL_RELEASE(RemoteLazyInputStreamStorage) + +/* static */ +Result<RefPtr<RemoteLazyInputStreamStorage>, nsresult> +RemoteLazyInputStreamStorage::Get() { + mozilla::StaticMutexAutoLock lock(gMutex); + if (gStorage) { + RefPtr<RemoteLazyInputStreamStorage> storage = gStorage; + return storage; + } + + return Err(NS_ERROR_NOT_INITIALIZED); +} + +/* static */ +void RemoteLazyInputStreamStorage::Initialize() { + mozilla::StaticMutexAutoLock lock(gMutex); + MOZ_ASSERT(!gStorage); + + gStorage = new RemoteLazyInputStreamStorage(); + + MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue( + "RemoteLazyInputStreamStorage", getter_AddRefs(gStorage->mTaskQueue))); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(gStorage, "xpcom-shutdown", false); + } +} + +NS_IMETHODIMP +RemoteLazyInputStreamStorage::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(!strcmp(aTopic, "xpcom-shutdown")); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "xpcom-shutdown"); + } + + mozilla::StaticMutexAutoLock lock(gMutex); + gStorage = nullptr; + return NS_OK; +} + +void RemoteLazyInputStreamStorage::AddStream(nsIInputStream* aInputStream, + const nsID& aID) { + MOZ_ASSERT(aInputStream); + + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::AddStream(%s) = %p", nsIDToCString(aID).get(), aInputStream)); + + UniquePtr<StreamData> data = MakeUnique<StreamData>(); + data->mInputStream = aInputStream; + + mozilla::StaticMutexAutoLock lock(gMutex); + mStorage.InsertOrUpdate(aID, std::move(data)); +} + +nsCOMPtr<nsIInputStream> RemoteLazyInputStreamStorage::ForgetStream( + const nsID& aID) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ForgetStream(%s)", nsIDToCString(aID).get())); + + UniquePtr<StreamData> entry; + + mozilla::StaticMutexAutoLock lock(gMutex); + mStorage.Remove(aID, &entry); + + if (!entry) { + return nullptr; + } + + return std::move(entry->mInputStream); +} + +bool RemoteLazyInputStreamStorage::HasStream(const nsID& aID) { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + return !!data; +} + +void RemoteLazyInputStreamStorage::GetStream(const nsID& aID, uint64_t aStart, + uint64_t aLength, + nsIInputStream** aInputStream) { + *aInputStream = nullptr; + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::GetStream(%s, %" PRIu64 " %" PRIu64 ")", + nsIDToCString(aID).get(), aStart, aLength)); + + nsCOMPtr<nsIInputStream> inputStream; + + // NS_CloneInputStream cannot be called when the mutex is locked because it + // can, recursively call GetStream() in case the child actor lives on the + // parent process. + { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return; + } + + inputStream = data->mInputStream; + } + + MOZ_ASSERT(inputStream); + + // We cannot return always the same inputStream because not all of them are + // able to be reused. Better to clone them. + + nsCOMPtr<nsIInputStream> clonedStream; + nsCOMPtr<nsIInputStream> replacementStream; + + nsresult rv = NS_CloneInputStream(inputStream, getter_AddRefs(clonedStream), + getter_AddRefs(replacementStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (replacementStream) { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + // data can be gone in the meantime. + if (!data) { + return; + } + + data->mInputStream = replacementStream; + } + + // Now it's the right time to apply a slice if needed. + if (aStart > 0 || aLength < UINT64_MAX) { + clonedStream = + new SlicedInputStream(clonedStream.forget(), aStart, aLength); + } + + clonedStream.forget(aInputStream); +} + +void RemoteLazyInputStreamStorage::StoreCallback( + const nsID& aID, RemoteLazyInputStreamParentCallback* aCallback) { + MOZ_ASSERT(aCallback); + + MOZ_LOG( + gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::StoreCallback(%s, %p)", nsIDToCString(aID).get(), aCallback)); + + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (data) { + MOZ_ASSERT(!data->mCallback); + data->mCallback = aCallback; + } +} + +already_AddRefed<RemoteLazyInputStreamParentCallback> +RemoteLazyInputStreamStorage::TakeCallback(const nsID& aID) { + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::TakeCallback(%s)", nsIDToCString(aID).get())); + + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return nullptr; + } + + RefPtr<RemoteLazyInputStreamParentCallback> callback; + data->mCallback.swap(callback); + return callback.forget(); +} + +void RemoteLazyInputStreamStorage::ActorCreated(const nsID& aID) { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return; + } + + size_t count = ++data->mActorCount; + + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ActorCreated(%s) = %zu", nsIDToCString(aID).get(), count)); +} + +void RemoteLazyInputStreamStorage::ActorDestroyed(const nsID& aID) { + UniquePtr<StreamData> entry; + { + mozilla::StaticMutexAutoLock lock(gMutex); + StreamData* data = mStorage.Get(aID); + if (!data) { + return; + } + + auto newCount = --data->mActorCount; + MOZ_LOG(gRemoteLazyStreamLog, LogLevel::Verbose, + ("Storage::ActorDestroyed(%s) = %zu (cb=%p)", + nsIDToCString(aID).get(), newCount, data->mCallback.get())); + + if (newCount == 0) { + mStorage.Remove(aID, &entry); + } + } + + if (entry && entry->mCallback) { + entry->mCallback->ActorDestroyed(aID); + } +} + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamStorage.h b/dom/file/ipc/RemoteLazyInputStreamStorage.h new file mode 100644 index 0000000000..296a8d9313 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamStorage.h @@ -0,0 +1,78 @@ +/* -*- 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/. */ + +#ifndef mozilla_RemoteLazyInputStreamStorage_h +#define mozilla_RemoteLazyInputStreamStorage_h + +#include "mozilla/RefPtr.h" +#include "nsClassHashtable.h" +#include "nsIObserver.h" + +class nsIInputStream; +struct nsID; + +namespace mozilla { + +class NS_NO_VTABLE RemoteLazyInputStreamParentCallback { + public: + virtual void ActorDestroyed(const nsID& aID) = 0; + + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + protected: + virtual ~RemoteLazyInputStreamParentCallback() = default; +}; + +class RemoteLazyInputStreamStorage final : public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + // This initializes the singleton and it must be called on the main-thread. + static void Initialize(); + + static Result<RefPtr<RemoteLazyInputStreamStorage>, nsresult> Get(); + + nsISerialEventTarget* TaskQueue() { return mTaskQueue; } + + void AddStream(nsIInputStream* aInputStream, const nsID& aID); + + // Removes and returns the stream corresponding to the nsID. May return a + // nullptr if there's no stream stored for the nsID. + nsCOMPtr<nsIInputStream> ForgetStream(const nsID& aID); + + bool HasStream(const nsID& aID); + + void GetStream(const nsID& aID, uint64_t aStart, uint64_t aLength, + nsIInputStream** aInputStream); + + void StoreCallback(const nsID& aID, + RemoteLazyInputStreamParentCallback* aCallback); + + already_AddRefed<RemoteLazyInputStreamParentCallback> TakeCallback( + const nsID& aID); + + void ActorCreated(const nsID& aID); + void ActorDestroyed(const nsID& aID); + + private: + RemoteLazyInputStreamStorage() = default; + ~RemoteLazyInputStreamStorage() = default; + + nsCOMPtr<nsISerialEventTarget> mTaskQueue; + + struct StreamData { + nsCOMPtr<nsIInputStream> mInputStream; + RefPtr<RemoteLazyInputStreamParentCallback> mCallback; + size_t mActorCount = 0; + }; + + nsClassHashtable<nsIDHashKey, StreamData> mStorage; +}; + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamStorage_h diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.cpp b/dom/file/ipc/RemoteLazyInputStreamThread.cpp new file mode 100644 index 0000000000..ec6c99952b --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamThread.cpp @@ -0,0 +1,237 @@ +/* -*- 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 "RemoteLazyInputStreamThread.h" + +#include "ErrorList.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TaskCategory.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "nsXPCOMPrivate.h" + +using namespace mozilla::ipc; + +namespace mozilla { + +namespace { + +StaticMutex gRemoteLazyThreadMutex; +StaticRefPtr<RemoteLazyInputStreamThread> gRemoteLazyThread; + +class ThreadInitializeRunnable final : public Runnable { + public: + ThreadInitializeRunnable() : Runnable("dom::ThreadInitializeRunnable") {} + + NS_IMETHOD + Run() override { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + MOZ_ASSERT(gRemoteLazyThread); + if (NS_WARN_IF(!gRemoteLazyThread->InitializeOnMainThread())) { + // RemoteLazyInputStreamThread::GetOrCreate might have handed out a + // pointer to our thread already at this point such that we cannot + // just do gRemoteLazyThread = nullptr; here. + MOZ_DIAGNOSTIC_ASSERT( + false, "Async gRemoteLazyThread->InitializeOnMainThread() failed."); + return NS_ERROR_FAILURE; + } + return NS_OK; + } +}; + +} // namespace + +NS_IMPL_ISUPPORTS(RemoteLazyInputStreamThread, nsIObserver, nsIEventTarget, + nsISerialEventTarget, nsIDirectTaskDispatcher) + +bool RLISThreadIsInOrBeyondShutdown() { + // ShutdownPhase::XPCOMShutdownThreads matches + // obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + return AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads); +} + +/* static */ +RemoteLazyInputStreamThread* RemoteLazyInputStreamThread::Get() { + if (RLISThreadIsInOrBeyondShutdown()) { + return nullptr; + } + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + return gRemoteLazyThread; +} + +/* static */ +RemoteLazyInputStreamThread* RemoteLazyInputStreamThread::GetOrCreate() { + if (RLISThreadIsInOrBeyondShutdown()) { + return nullptr; + } + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + if (!gRemoteLazyThread) { + gRemoteLazyThread = new RemoteLazyInputStreamThread(); + if (!gRemoteLazyThread->Initialize()) { + gRemoteLazyThread = nullptr; + } + } + + return gRemoteLazyThread; +} + +bool RemoteLazyInputStreamThread::Initialize() { + nsCOMPtr<nsIThread> thread; + nsresult rv = NS_NewNamedThread("RemoteLzyStream", getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + mThread = thread; + + if (!NS_IsMainThread()) { + RefPtr<Runnable> runnable = new ThreadInitializeRunnable(); + nsresult rv = + SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()); + return !NS_WARN_IF(NS_FAILED(rv)); + } + + return InitializeOnMainThread(); +} + +bool RemoteLazyInputStreamThread::InitializeOnMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return false; + } + + nsresult rv = + obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + return !NS_WARN_IF(NS_FAILED(rv)); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + if (mThread) { + mThread->Shutdown(); + mThread = nullptr; + } + + gRemoteLazyThread = nullptr; + + return NS_OK; +} + +// nsIEventTarget + +NS_IMETHODIMP_(bool) +RemoteLazyInputStreamThread::IsOnCurrentThreadInfallible() { + return mThread->IsOnCurrentThread(); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::IsOnCurrentThread(bool* aRetval) { + return mThread->IsOnCurrentThread(aRetval); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) { + if (RLISThreadIsInOrBeyondShutdown()) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + return mThread->Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DispatchFromScript(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DelayedDispatch(already_AddRefed<nsIRunnable>, + uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::RegisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::UnregisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +RemoteLazyInputStreamThread::DispatchDirectTask( + already_AddRefed<nsIRunnable> aRunnable) { + nsCOMPtr<nsIRunnable> runnable(aRunnable); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->DispatchDirectTask(runnable.forget()); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP RemoteLazyInputStreamThread::DrainDirectTasks() { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->DrainDirectTasks(); + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP RemoteLazyInputStreamThread::HaveDirectTasks(bool* aValue) { + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + + nsCOMPtr<nsIDirectTaskDispatcher> dispatcher = do_QueryInterface(mThread); + + if (dispatcher) { + return dispatcher->HaveDirectTasks(aValue); + } + + return NS_ERROR_FAILURE; +} + +bool IsOnDOMFileThread() { + MOZ_ASSERT(!RLISThreadIsInOrBeyondShutdown()); + + StaticMutexAutoLock lock(gRemoteLazyThreadMutex); + MOZ_ASSERT(gRemoteLazyThread); + + return gRemoteLazyThread->IsOnCurrentThreadInfallible(); +} + +void AssertIsOnDOMFileThread() { MOZ_ASSERT(IsOnDOMFileThread()); } + +} // namespace mozilla diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.h b/dom/file/ipc/RemoteLazyInputStreamThread.h new file mode 100644 index 0000000000..378cb09009 --- /dev/null +++ b/dom/file/ipc/RemoteLazyInputStreamThread.h @@ -0,0 +1,52 @@ +/* -*- 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/. */ + +#ifndef mozilla_RemoteLazyInputStreamThread_h +#define mozilla_RemoteLazyInputStreamThread_h + +#include "mozilla/RemoteLazyInputStreamChild.h" +#include "nsIEventTarget.h" +#include "nsIObserver.h" +#include "nsTArray.h" + +class nsIThread; + +namespace mozilla { + +class RemoteLazyInputStreamChild; + +// XXX Rename this class since it's used by LSNG too. +class RemoteLazyInputStreamThread final : public nsIObserver, + public nsISerialEventTarget, + public nsIDirectTaskDispatcher { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIEVENTTARGET + NS_DECL_NSISERIALEVENTTARGET + NS_DECL_NSIDIRECTTASKDISPATCHER + + static RemoteLazyInputStreamThread* Get(); + + static RemoteLazyInputStreamThread* GetOrCreate(); + + bool Initialize(); + + bool InitializeOnMainThread(); + + private: + ~RemoteLazyInputStreamThread() = default; + + nsCOMPtr<nsIThread> mThread; +}; + +bool IsOnDOMFileThread(); + +void AssertIsOnDOMFileThread(); + +} // namespace mozilla + +#endif // mozilla_RemoteLazyInputStreamThread_h diff --git a/dom/file/ipc/TemporaryIPCBlobChild.cpp b/dom/file/ipc/TemporaryIPCBlobChild.cpp new file mode 100644 index 0000000000..7c7df55d81 --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobChild.cpp @@ -0,0 +1,86 @@ +/* -*- 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 "TemporaryIPCBlobChild.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/MutableBlobStorage.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include <private/pprio.h> + +namespace mozilla::dom { + +TemporaryIPCBlobChild::TemporaryIPCBlobChild(MutableBlobStorage* aStorage) + : mMutableBlobStorage(aStorage), mActive(true) { + MOZ_ASSERT(aStorage); +} + +TemporaryIPCBlobChild::~TemporaryIPCBlobChild() = default; + +mozilla::ipc::IPCResult TemporaryIPCBlobChild::RecvFileDesc( + const FileDescriptor& aFD) { + MOZ_ASSERT(mActive); + + auto rawFD = aFD.ClonePlatformHandle(); + PRFileDesc* prfile = PR_ImportFile(PROsfd(rawFD.release())); + + mMutableBlobStorage->TemporaryFileCreated(prfile); + mMutableBlobStorage = nullptr; + return IPC_OK(); +} + +mozilla::ipc::IPCResult TemporaryIPCBlobChild::Recv__delete__( + const IPCBlobOrError& aBlobOrError) { + mActive = false; + mMutableBlobStorage = nullptr; + + if (aBlobOrError.type() == IPCBlobOrError::TIPCBlob) { + // This must be always deserialized. + RefPtr<BlobImpl> blobImpl = + IPCBlobUtils::Deserialize(aBlobOrError.get_IPCBlob()); + MOZ_ASSERT(blobImpl); + + if (mCallback) { + mCallback->OperationSucceeded(blobImpl); + } + } else if (mCallback) { + MOZ_ASSERT(aBlobOrError.type() == IPCBlobOrError::Tnsresult); + mCallback->OperationFailed(aBlobOrError.get_nsresult()); + } + + mCallback = nullptr; + + return IPC_OK(); +} + +void TemporaryIPCBlobChild::ActorDestroy(ActorDestroyReason aWhy) { + mActive = false; + mMutableBlobStorage = nullptr; + + if (mCallback) { + mCallback->OperationFailed(NS_ERROR_FAILURE); + mCallback = nullptr; + } +} + +void TemporaryIPCBlobChild::AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType, + PRFileDesc* aFD) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(!mCallback); + + if (!mActive) { + aCallback->OperationFailed(NS_ERROR_FAILURE); + return; + } + + FileDescriptor fdd = FileDescriptor( + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(aFD))); + + mCallback = aCallback; + SendOperationDone(aContentType, fdd); +} + +} // namespace mozilla::dom diff --git a/dom/file/ipc/TemporaryIPCBlobChild.h b/dom/file/ipc/TemporaryIPCBlobChild.h new file mode 100644 index 0000000000..a909ee0d53 --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobChild.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_TemporaryIPCBlobChild_h +#define mozilla_dom_TemporaryIPCBlobChild_h + +#include "mozilla/dom/PTemporaryIPCBlob.h" +#include "mozilla/dom/PTemporaryIPCBlobChild.h" + +namespace mozilla::dom { + +class BlobImpl; +class MutableBlobStorage; + +class TemporaryIPCBlobChildCallback { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void OperationSucceeded(BlobImpl* aBlobImpl) = 0; + virtual void OperationFailed(nsresult aRv) = 0; +}; + +class TemporaryIPCBlobChild final : public PTemporaryIPCBlobChild { + friend class PTemporaryIPCBlobChild; + + public: + NS_INLINE_DECL_REFCOUNTING(TemporaryIPCBlobChild) + + explicit TemporaryIPCBlobChild(MutableBlobStorage* aStorage); + + void AskForBlob(TemporaryIPCBlobChildCallback* aCallback, + const nsACString& aContentType, PRFileDesc* aFD); + + private: + ~TemporaryIPCBlobChild() override; + + mozilla::ipc::IPCResult RecvFileDesc(const FileDescriptor& aFD); + + mozilla::ipc::IPCResult Recv__delete__(const IPCBlobOrError& aBlobOrError); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr<MutableBlobStorage> mMutableBlobStorage; + RefPtr<TemporaryIPCBlobChildCallback> mCallback; + bool mActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TemporaryIPCBlobChild_h diff --git a/dom/file/ipc/TemporaryIPCBlobParent.cpp b/dom/file/ipc/TemporaryIPCBlobParent.cpp new file mode 100644 index 0000000000..bd2c6beefc --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobParent.cpp @@ -0,0 +1,102 @@ +/* -*- 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 "private/pprio.h" +#include "TemporaryIPCBlobParent.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "nsAnonymousTemporaryFile.h" +#include "TemporaryFileBlobImpl.h" +#include "mozilla/dom/IPCBlobUtils.h" + +namespace mozilla::dom { + +TemporaryIPCBlobParent::TemporaryIPCBlobParent() : mActive(true) {} + +TemporaryIPCBlobParent::~TemporaryIPCBlobParent() { + // If we still have mFile, let's remove it. + if (mFile) { + mFile->Remove(false); + } +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::CreateAndShareFile() { + MOZ_ASSERT(mActive); + MOZ_ASSERT(!mFile); + + nsresult rv = NS_OpenAnonymousTemporaryNsIFile(getter_AddRefs(mFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return SendDeleteError(rv); + } + + PRFileDesc* fd; + rv = mFile->OpenNSPRFileDesc(PR_RDWR, PR_IRWXU, &fd); + if (NS_WARN_IF(NS_FAILED(rv))) { + return SendDeleteError(rv); + } + + FileDescriptor fdd = FileDescriptor( + FileDescriptor::PlatformHandleType(PR_FileDesc2NativeHandle(fd))); + + // The FileDescriptor object owns a duplicate of the file handle; we + // must close the original (and clean up the NSPR descriptor). + PR_Close(fd); + + (void)SendFileDesc(fdd); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::RecvOperationFailed() { + MOZ_ASSERT(mActive); + mActive = false; + + // Nothing to do. + (void)Send__delete__(this, NS_ERROR_FAILURE); + return IPC_OK(); +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::RecvOperationDone( + const nsCString& aContentType, const FileDescriptor& aFD) { + MOZ_ASSERT(mActive); + mActive = false; + + // We have received a file descriptor because in this way we have kept the + // file locked on windows during the IPC communication. After the creation of + // the TemporaryFileBlobImpl, this prfile can be closed. + auto rawFD = aFD.ClonePlatformHandle(); + PRFileDesc* prfile = PR_ImportFile(PROsfd(rawFD.release())); + + // Let's create the BlobImpl. + nsCOMPtr<nsIFile> file = std::move(mFile); + + RefPtr<TemporaryFileBlobImpl> blobImpl = + new TemporaryFileBlobImpl(file, NS_ConvertUTF8toUTF16(aContentType)); + + PR_Close(prfile); + + IPCBlob ipcBlob; + nsresult rv = IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + (void)Send__delete__(this, NS_ERROR_FAILURE); + return IPC_OK(); + } + + (void)Send__delete__(this, ipcBlob); + return IPC_OK(); +} + +void TemporaryIPCBlobParent::ActorDestroy(ActorDestroyReason aWhy) { + mActive = false; +} + +mozilla::ipc::IPCResult TemporaryIPCBlobParent::SendDeleteError(nsresult aRv) { + MOZ_ASSERT(mActive); + mActive = false; + + (void)Send__delete__(this, aRv); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/file/ipc/TemporaryIPCBlobParent.h b/dom/file/ipc/TemporaryIPCBlobParent.h new file mode 100644 index 0000000000..2609e8d820 --- /dev/null +++ b/dom/file/ipc/TemporaryIPCBlobParent.h @@ -0,0 +1,43 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_TemporaryIPCBlobParent_h +#define mozilla_dom_TemporaryIPCBlobParent_h + +#include "mozilla/dom/PTemporaryIPCBlob.h" +#include "mozilla/dom/PTemporaryIPCBlobParent.h" + +class nsIFile; + +namespace mozilla::dom { + +class TemporaryIPCBlobParent final : public PTemporaryIPCBlobParent { + friend class PTemporaryIPCBlobParent; + + public: + explicit TemporaryIPCBlobParent(); + + mozilla::ipc::IPCResult CreateAndShareFile(); + + private: + ~TemporaryIPCBlobParent() override; + + mozilla::ipc::IPCResult RecvOperationFailed(); + + mozilla::ipc::IPCResult RecvOperationDone(const nsCString& aContentType, + const FileDescriptor& aFD); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult SendDeleteError(nsresult aRv); + + nsCOMPtr<nsIFile> mFile; + bool mActive; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TemporaryIPCBlobParent_h diff --git a/dom/file/ipc/moz.build b/dom/file/ipc/moz.build new file mode 100644 index 0000000000..21c2d50cac --- /dev/null +++ b/dom/file/ipc/moz.build @@ -0,0 +1,69 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: File") + +XPIDL_SOURCES += [ + "mozIRemoteLazyInputStream.idl", +] + +XPIDL_MODULE = "dom" + +EXPORTS.mozilla.dom += [ + "FileCreatorChild.h", + "FileCreatorParent.h", + "IPCBlobUtils.h", + "TemporaryIPCBlobChild.h", + "TemporaryIPCBlobParent.h", +] + +EXPORTS.mozilla += [ + "RemoteLazyInputStream.h", + "RemoteLazyInputStreamChild.h", + "RemoteLazyInputStreamParent.h", + "RemoteLazyInputStreamStorage.h", + "RemoteLazyInputStreamThread.h", +] + +UNIFIED_SOURCES += [ + "FileCreatorChild.cpp", + "FileCreatorParent.cpp", + "IPCBlobUtils.cpp", + "RemoteLazyInputStream.cpp", + "RemoteLazyInputStreamChild.cpp", + "RemoteLazyInputStreamParent.cpp", + "RemoteLazyInputStreamStorage.cpp", + "RemoteLazyInputStreamThread.cpp", + "TemporaryIPCBlobChild.cpp", + "TemporaryIPCBlobParent.cpp", +] + +IPDL_SOURCES += [ + "IPCBlob.ipdlh", + "PFileCreator.ipdl", + "PRemoteLazyInputStream.ipdl", + "PTemporaryIPCBlob.ipdl", +] + +LOCAL_INCLUDES += [ + "/dom/file", + "/dom/ipc", + "/xpcom/build", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"] +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] diff --git a/dom/file/ipc/mozIRemoteLazyInputStream.idl b/dom/file/ipc/mozIRemoteLazyInputStream.idl new file mode 100644 index 0000000000..8303b2e0fc --- /dev/null +++ b/dom/file/ipc/mozIRemoteLazyInputStream.idl @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIInputStream; + +/* + * A simple interface to get the underlying stream from an + * RemoteLazyInputStream. + */ +[scriptable, builtinclass, uuid(4125585f-b0c2-4964-a83c-4b0d99f26d49)] +interface mozIRemoteLazyInputStream : nsISupports +{ + /** + * Attempts to take the internal stream out of this mozIRemoteLazyInputStream. + * Throws NS_BASE_STREAM_WOULD_BLOCK if the stream isn't available yet, and + * NS_BASE_STREAM_CLOSED if it was already closed. + */ + [noscript] nsIInputStream TakeInternalStream(); + + /** + * If this RemoteLazyInputStream is actively backed by an actor, get the + * underlying actor's ID. Will throw if the underlying actor is no longer + * available. + */ + [noscript] readonly attribute nsIDRef internalStreamID; +}; diff --git a/dom/file/ipc/tests/browser.ini b/dom/file/ipc/tests/browser.ini new file mode 100644 index 0000000000..f6fcdc37a4 --- /dev/null +++ b/dom/file/ipc/tests/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + empty.html + +[browser_ipcBlob.js] +[browser_ipcBlob_temporary.js] +support-files = temporary.sjs diff --git a/dom/file/ipc/tests/browser_ipcBlob.js b/dom/file/ipc/tests/browser_ipcBlob.js new file mode 100644 index 0000000000..17fe31e0bd --- /dev/null +++ b/dom/file/ipc/tests/browser_ipcBlob.js @@ -0,0 +1,253 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +requestLongerTimeout(3); + +const BASE_URI = "http://mochi.test:8888/browser/dom/file/ipc/tests/empty.html"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.partition.bloburl_per_agent_cluster", false]], + }); +}); + +// More than 1mb memory blob childA-parent-childB. +add_task(async function test_CtoPtoC_big() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blob = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + let blob = new Blob([new Array(1024 * 1024).join("123456789ABCDEF")]); + return blob; + }); + + ok(blob, "CtoPtoC-big: We have a blob!"); + is( + blob.size, + new Array(1024 * 1024).join("123456789ABCDEF").length, + "CtoPtoC-big: The size matches" + ); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [blob], function (blob) { + return new Promise(resolve => { + let fr = new content.FileReader(); + fr.readAsText(blob); + fr.onloadend = function () { + resolve(fr.result == new Array(1024 * 1024).join("123456789ABCDEF")); + }; + }); + }); + + ok(status, "CtoPtoC-big: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Less than 1mb memory blob childA-parent-childB. +add_task(async function test_CtoPtoC_small() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blob = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + let blob = new Blob(["hello world!"]); + return blob; + }); + + ok(blob, "CtoPtoC-small: We have a blob!"); + is(blob.size, "hello world!".length, "CtoPtoC-small: The size matches"); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [blob], function (blob) { + return new Promise(resolve => { + let fr = new content.FileReader(); + fr.readAsText(blob); + fr.onloadend = function () { + resolve(fr.result == "hello world!"); + }; + }); + }); + + ok(status, "CtoPtoC-small: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// More than 1mb memory blob childA-parent-childB: BroadcastChannel +add_task(async function test_CtoPtoC_bc_big() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function () { + bc.postMessage( + new Blob([new Array(1024 * 1024).join("123456789ABCDEF")]) + ); + }; + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [], function () { + return new Promise(resolve => { + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function (e) { + let fr = new content.FileReader(); + fr.readAsText(e.data); + fr.onloadend = function () { + resolve(fr.result == new Array(1024 * 1024).join("123456789ABCDEF")); + }; + }; + + bc.postMessage("GO!"); + }); + }); + + ok(status, "CtoPtoC-broadcastChannel-big: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Less than 1mb memory blob childA-parent-childB: BroadcastChannel +add_task(async function test_CtoPtoC_bc_small() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function () { + bc.postMessage(new Blob(["hello world!"])); + }; + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [], function () { + return new Promise(resolve => { + var bc = new content.BroadcastChannel("test"); + bc.onmessage = function (e) { + let fr = new content.FileReader(); + fr.readAsText(e.data); + fr.onloadend = function () { + resolve(fr.result == "hello world!"); + }; + }; + + bc.postMessage("GO!"); + }); + }); + + ok(status, "CtoPtoC-broadcastChannel-small: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// blob URL childA-parent-childB +add_task(async function test_CtoPtoC_bc_small() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blobURL = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + return content.URL.createObjectURL(new content.Blob(["hello world!"])); + }); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn( + browser2, + [blobURL], + function (blobURL) { + return new Promise(resolve => { + var xhr = new content.XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.onloadend = function () { + resolve(xhr.response == "hello world!"); + }; + + xhr.send(); + }); + } + ); + + ok(status, "CtoPtoC-blobURL: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Multipart Blob childA-parent-childB. +add_task(async function test_CtoPtoC_multipart() { + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let blob = await SpecialPowers.spawn(browser1, [], function () { + Cu.importGlobalProperties(["Blob"]); + return new Blob(["!"]); + }); + + ok(blob, "CtoPtoC-multipart: We have a blob!"); + is(blob.size, "!".length, "CtoPtoC-multipart: The size matches"); + + let newBlob = new Blob(["world", blob]); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + let status = await SpecialPowers.spawn(browser2, [newBlob], function (blob) { + Cu.importGlobalProperties(["Blob"]); + return new Promise(resolve => { + let fr = new content.FileReader(); + fr.readAsText(new Blob(["hello ", blob])); + fr.onloadend = function () { + resolve(fr.result == "hello world!"); + }; + }); + }); + + ok(status, "CtoPtoC-multipart: Data match!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +// Multipart Blob childA-parent with a max size +add_task(async function test_CtoPsize_multipart() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser = gBrowser.getBrowserForTab(tab); + + let blob = await SpecialPowers.spawn(browser, [], function () { + Cu.importGlobalProperties(["Blob"]); + + let data = new Array(1024 * 512).join("A"); + let blob1 = new Blob([data]); + let blob2 = new Blob([data]); + let blob3 = new Blob([data]); + + return new Blob([blob1, blob2, blob3]); + }); + + ok(blob, "CtoPsize-multipart: We have a blob!"); + is( + blob.size, + new Array(1024 * 512).join("A").length * 3, + "CtoPsize-multipart: The size matches" + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/file/ipc/tests/browser_ipcBlob_temporary.js b/dom/file/ipc/tests/browser_ipcBlob_temporary.js new file mode 100644 index 0000000000..cc65768140 --- /dev/null +++ b/dom/file/ipc/tests/browser_ipcBlob_temporary.js @@ -0,0 +1,115 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +requestLongerTimeout(3); + +const BASE_URI = "http://mochi.test:8888/browser/dom/file/ipc/tests/empty.html"; + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.blob.memoryToTemporaryFile", 1], + ["dom.ipc.processCount", 4], + ], + }); + + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser1 = gBrowser.getBrowserForTab(tab1); + + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_URI); + let browser2 = gBrowser.getBrowserForTab(tab2); + + await SpecialPowers.spawn(browser2, [], function () { + content.window.testPromise = new content.window.Promise(resolve => { + let bc = new content.window.BroadcastChannel("foobar"); + bc.onmessage = e => { + function realTest() { + return new content.window.Promise(resolve => { + let count = 10; + for (let i = 0; i < count; ++i) { + info("FileReader at the same time: " + i); + let fr = new content.window.FileReader(); + fr.readAsText(e.data); + fr.onerror = () => { + ok(false, "Something wrong happened."); + }; + + fr.onloadend = () => { + is(fr.result.length, e.data.size, "FileReader worked fine."); + if (!--count) { + resolve(true); + } + }; + } + }); + } + + let promises = []; + for (let i = 0; i < 5; ++i) { + promises.push(realTest()); + } + + Promise.all(promises).then(() => { + resolve(true); + }); + }; + }); + }); + + let status = await SpecialPowers.spawn(browser1, [], function () { + let p = new content.window.Promise(resolve => { + let xhr = new content.window.XMLHttpRequest(); + xhr.open("GET", "temporary.sjs", true); + xhr.responseType = "blob"; + xhr.onload = () => { + resolve(xhr.response); + }; + xhr.send(); + }); + + return p.then(blob => { + function realTest() { + return new content.window.Promise(resolve => { + info("Let's broadcast the blob..."); + let bc = new content.window.BroadcastChannel("foobar"); + bc.postMessage(blob); + + info("Here the test..."); + let count = 10; + for (let i = 0; i < count; ++i) { + info("FileReader at the same time: " + i); + let fr = new content.window.FileReader(); + fr.readAsText(blob); + fr.onerror = () => { + ok(false, "Something wrong happened."); + }; + + fr.onloadend = () => { + is(fr.result.length, blob.size, "FileReader worked fine."); + if (!--count) { + resolve(true); + } + }; + } + }); + } + + let promises = []; + for (let i = 0; i < 5; ++i) { + promises.push(realTest()); + } + + return Promise.all(promises); + }); + }); + + ok(status, "All good for tab1!"); + + status = await SpecialPowers.spawn(browser2, [], function () { + return content.window.testPromise; + }); + + ok(status, "All good for tab2!"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/dom/file/ipc/tests/empty.html b/dom/file/ipc/tests/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/file/ipc/tests/empty.html diff --git a/dom/file/ipc/tests/green.jpg b/dom/file/ipc/tests/green.jpg Binary files differnew file mode 100644 index 0000000000..48c454d27c --- /dev/null +++ b/dom/file/ipc/tests/green.jpg diff --git a/dom/file/ipc/tests/mochitest.ini b/dom/file/ipc/tests/mochitest.ini new file mode 100644 index 0000000000..99b885762a --- /dev/null +++ b/dom/file/ipc/tests/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = script_file.js + +[test_ipcBlob_fileReaderSync.html] +[test_ipcBlob_workers.html] +[test_ipcBlob_createImageBitmap.html] +support-files = green.jpg +skip-if = + http3 +[test_ipcBlob_emptyMultiplex.html] +[test_ipcBlob_mixedMultiplex.html] +support-files = ok.sjs diff --git a/dom/file/ipc/tests/ok.sjs b/dom/file/ipc/tests/ok.sjs new file mode 100644 index 0000000000..42f65733d9 --- /dev/null +++ b/dom/file/ipc/tests/ok.sjs @@ -0,0 +1,11 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.write(request.getHeader("Content-Length")); +} diff --git a/dom/file/ipc/tests/script_file.js b/dom/file/ipc/tests/script_file.js new file mode 100644 index 0000000000..b671d46f39 --- /dev/null +++ b/dom/file/ipc/tests/script_file.js @@ -0,0 +1,53 @@ +/* eslint-env mozilla/chrome-script */ + +Cu.importGlobalProperties(["File"]); + +addMessageListener("file.open", function (e) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("ipc_fileReader_testing"); + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + + var fileData = "Hello World!"; + outStream.write(fileData, fileData.length); + outStream.close(); + + File.createFromNsIFile(testFile).then(function (file) { + sendAsyncMessage("file.opened", { file }); + }); +}); + +addMessageListener("emptyfile.open", function (e) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("ipc_fileReader_testing"); + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + testFile, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + + File.createFromNsIFile(testFile).then(function (file) { + sendAsyncMessage("emptyfile.opened", { file }); + }); +}); diff --git a/dom/file/ipc/tests/temporary.sjs b/dom/file/ipc/tests/temporary.sjs new file mode 100644 index 0000000000..76db4e2f2e --- /dev/null +++ b/dom/file/ipc/tests/temporary.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + + var data = new Array(1024 * 64).join("1234567890ABCDEF"); + response.bodyOutputStream.write(data, data.length); +} diff --git a/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html b/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html new file mode 100644 index 0000000000..868888c529 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_createImageBitmap.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IPCBlob and CreateImageBitmap</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function test_mainThread() { + let bc = new BroadcastChannel('testMainThread'); + bc.onmessage = e => { + createImageBitmap(e.data).then(image => { + ok(image.height, "this image has a valid size."); + }, () => { + ok(false, "error creating the image!"); + }).then(next); + } + + fetch('green.jpg').then(r => r.blob()).then(blob => { + let bc = new BroadcastChannel('testMainThread'); + bc.postMessage(blob); + }); +} + +function test_worker() { + function workerScript() { + function ok(a, msg) { postMessage({ type: 'test', status: !!a, msg }); }; + function finish() { postMessage({ type: 'finish' }); }; + + let bc = new BroadcastChannel('testWorker'); + bc.onmessage = e => { + createImageBitmap(e.data).then(image => { + ok(image.height, "this image has a valid size."); + }, () => { + ok(false, "error creating the image!"); + }).then(finish); + } + + fetch('http://mochi.test:8888/tests/dom/file/ipc/tests/green.jpg').then(r => r.blob()).then(blob => { + let bc = new BroadcastChannel('testWorker'); + bc.postMessage(blob); + }); + } + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.onmessage = event => { + if (event.data.type == 'test') { + ok(event.data.status, event.data.msg); + return; + } + + if (event.data.type == 'finish') { + next(); + } + } +} + +let tests = [ + test_mainThread, + test_worker, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + let test = tests.shift(); + test(); +} + +next(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html b/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html new file mode 100644 index 0000000000..0487069467 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_emptyMultiplex.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test an empty IPCBlob together with other parts</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +function checkContent(msg, content) { + return new Promise(resolve => { + let fr = new FileReader(); + fr.readAsText(new Blob(content)); + fr.onloadend = () => { + is(fr.result, "Hello world!", "The content matches: " + msg); + resolve(); + }; + }); +} + +SimpleTest.waitForExplicitFinish(); + +let url = SimpleTest.getTestFileURL("script_file.js"); + +let script = SpecialPowers.loadChromeScript(url); +script.addMessageListener("emptyfile.opened", message => { + checkContent("middle", ["Hello ", message.file, "world!"]). + then(() => checkContent("begin", [message.file, "Hello world!"])). + then(() => checkContent("end", ["Hello world!", message.file])). + then(() => checkContent("random", [message.file, message.file, "Hello world!", message.file])). + then(() => checkContent("random 2", [message.file, message.file, "Hello ", + message.file, "world", message.file, + message.file, "!", message.file, "", + message.file, message.file])). + then(SimpleTest.finish); +}); + +script.sendAsyncMessage("emptyfile.open"); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html b/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html new file mode 100644 index 0000000000..f37d4b79ed --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_fileReaderSync.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IPCBlob and FileReaderSync</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +function workerScript() { + onmessage = function(event) { + let readerMemoryBlob = new FileReaderSync(); + let status = readerMemoryBlob.readAsText(new Blob(['hello world'])) == 'hello world'; + postMessage({ status, message: "FileReaderSync with memory blob still works" }); + + let readerIPCBlob1 = new FileReaderSync(); + postMessage({ blob: event.data, method: 'readAsText', + data: readerIPCBlob1.readAsText(event.data)}); + + let readerIPCBlob2 = new FileReaderSync(); + postMessage({ blob: event.data, method: 'readAsArrayBuffer', + data: readerIPCBlob2.readAsArrayBuffer(event.data)}); + + let readerIPCBlob3 = new FileReaderSync(); + postMessage({ blob: event.data, method: 'readAsDataURL', + data: readerIPCBlob3.readAsDataURL(event.data)}); + + let multipartBlob = new Blob(['wow', event.data]); + + let readerIPCBlobMultipart1 = new FileReaderSync(); + postMessage({ blob: multipartBlob, method: 'readAsText', + data: readerIPCBlobMultipart1.readAsText(multipartBlob)}); + + let readerIPCBlobMultipart2 = new FileReaderSync(); + postMessage({ blob: multipartBlob, method: 'readAsArrayBuffer', + data: readerIPCBlobMultipart2.readAsArrayBuffer(multipartBlob)}); + + let readerIPCBlobMultipart3 = new FileReaderSync(); + postMessage({ blob: multipartBlob, method: 'readAsDataURL', + data: readerIPCBlobMultipart3.readAsDataURL(multipartBlob)}); + + postMessage({ finish: true }); + } +} + +let completed = false; +let pendingTasks = 0; +function maybeFinish() { + if (completed && !pendingTasks) { + SimpleTest.finish(); + } +} + +let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); +let worker = new Worker(workerUrl); +worker.onmessage = event => { + if ("status" in event.data) { + ok(event.data.status, event.data.message); + return; + } + + if ("blob" in event.data) { + let fr = new FileReader(); + fr[event.data.method](event.data.blob); + ++pendingTasks; + fr.onload = () => { + if (event.data.method != 'readAsArrayBuffer') { + is(event.data.data, fr.result, "The file has been read"); + } else { + is(event.data.data.byteLength, fr.result.byteLength, "The file has been read"); + } + --pendingTasks; + maybeFinish(); + } + + return; + } + + if ("finish" in event.data) { + completed = true; + maybeFinish(); + } +}; + +let url = SimpleTest.getTestFileURL("script_file.js"); +let script = SpecialPowers.loadChromeScript(url); +script.addMessageListener("file.opened", message => { + worker.postMessage(message.file); +}); + +script.sendAsyncMessage("file.open"); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html b/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html new file mode 100644 index 0000000000..c8e046fa88 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_mixedMultiplex.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test an empty IPCBlob together with other parts</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +let url = SimpleTest.getTestFileURL("script_file.js"); +let data = new Array(1024*1024).join('A'); + +let script = SpecialPowers.loadChromeScript(url); +script.addMessageListener("file.opened", message => { + let blob = new Blob([data]); + + let form = new FormData(); + form.append("blob1", blob); + form.append("blob2", message.file); + form.append("blob3", blob); + + fetch("ok.sjs", { + method: "POST", + body: form, + }) + .then(r => r.text()) + .then(r => { + ok(parseInt(r, 10) > (data.length * 2), "We have data"); + }) + . then(SimpleTest.finish); +}); + +script.sendAsyncMessage("file.open"); +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/file/ipc/tests/test_ipcBlob_workers.html b/dom/file/ipc/tests/test_ipcBlob_workers.html new file mode 100644 index 0000000000..a473948f25 --- /dev/null +++ b/dom/file/ipc/tests/test_ipcBlob_workers.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IPCBlob and Workers</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +function test_workerOwner() { + info("test_workerOwner"); + + function workerScript() { + onmessage = e => { + e.ports[0].onmessage = event => { + let reader = new FileReader(); + reader.readAsText(event.data); + reader.onloadend = () => { + let status = reader.result == 'hello world'; + postMessage(status); + } + } + } + } + + let mc = new MessageChannel(); + mc.port1.postMessage(new Blob(['hello world'])); + + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.postMessage("", [mc.port2]); + worker.onmessage = event => { + ok(event.data, "All is done!"); + next(); + } +} + +function test_workerToMainThread() { + info("test_workerToMainThread"); + function workerScript() { + onmessage = e => { + e.ports[0].onmessage = event => { + postMessage(event.data); + } + } + } + + let mc = new MessageChannel(); + mc.port1.postMessage(new Blob(['hello world'])); + + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.postMessage("", [mc.port2]); + worker.onmessage = event => { + info("Blob received back, terminate the worker and force GC"); + worker.terminate(); + worker = null; + SpecialPowers.forceGC(); + + var fr = new FileReader(); + fr.readAsText(event.data); + fr.onloadend = () => { + is(fr.result, "hello world", "Data matches"); + next(); + } + } +} + +function test_workerOwnerPlusFileReaderSync() { + info("test_workerOwnerPlusFileReaderSync"); + + function workerScript() { + onmessage = e => { + e.ports[0].onmessage = event => { + let reader = new FileReaderSync(); + let status = reader.readAsText(event.data) == 'hello world'; + postMessage(status); + } + } + } + + let mc = new MessageChannel(); + mc.port1.postMessage(new Blob(['hello world'])); + + let workerUrl = URL.createObjectURL(new Blob(["(", workerScript.toString(), ")()"])); + let worker = new Worker(workerUrl); + + worker.postMessage("", [mc.port2]); + worker.onmessage = event => { + ok(event.data, "All is done!"); + next(); + } +} + +var tests = [ + test_workerOwner, + test_workerToMainThread, + test_workerOwnerPlusFileReaderSync, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); + +</script> +</pre> +</body> +</html> |