зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1642454: Implement IOUtils read and writeAtomic methods r=barret,smaug,Gijs
This patch introduces a minimal, asynchronous Web IDL interface for reading/writing whole files in privileged chrome code (main-thread and web workers). All I/O is performed on a background thread. Pending I/O blocks Firefox shutdown. Differential Revision: https://phabricator.services.mozilla.com/D78134
This commit is contained in:
Родитель
df2678e18f
Коммит
16fbac6f70
|
@ -0,0 +1,54 @@
|
|||
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* 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/.
|
||||
*/
|
||||
[ChromeOnly, Exposed=(Window, Worker)]
|
||||
namespace IOUtils {
|
||||
/**
|
||||
* Reads up to |maxBytes| of the file at |path|. If |maxBytes| is unspecified,
|
||||
* the entire file is read.
|
||||
*
|
||||
* @param path A forward-slash separated path.
|
||||
* @param maxBytes The max bytes to read from the file at path.
|
||||
*/
|
||||
Promise<Uint8Array> read(DOMString path, optional unsigned long maxBytes);
|
||||
/**
|
||||
* Atomically write |data| to a file at |path|. This operation attempts to
|
||||
* ensure that until the data is entirely written to disk, the destination
|
||||
* file is not modified.
|
||||
*
|
||||
* This is generally accomplished by writing to a temporary file, then
|
||||
* performing an overwriting move.
|
||||
*
|
||||
* @param path A forward-slash separated path.
|
||||
* @param data Data to write to the file at path.
|
||||
*/
|
||||
Promise<unsigned long long> writeAtomic(DOMString path, Uint8Array data, optional WriteAtomicOptions options = {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to be passed to the |IOUtils.writeAtomic| method.
|
||||
*/
|
||||
dictionary WriteAtomicOptions {
|
||||
/**
|
||||
* If specified, backup the destination file to this path before writing.
|
||||
*/
|
||||
DOMString backupFile;
|
||||
/**
|
||||
* If specified, write the data to a file at |tmpPath|. Once the write is
|
||||
* complete, the destination will be overwritten by a move.
|
||||
*/
|
||||
DOMString tmpPath;
|
||||
/**
|
||||
* If true, fail if the destination already exists.
|
||||
*/
|
||||
boolean noOverwrite = false;
|
||||
/**
|
||||
* If true, force the OS to write its internal buffers to the disk.
|
||||
* This is considerably slower for the whole system, but safer in case of
|
||||
* an improper system shutdown (e.g. due to a kernel panic) or device
|
||||
* disconnection before the buffers are flushed.
|
||||
*/
|
||||
boolean flush = false;
|
||||
};
|
|
@ -48,6 +48,7 @@ WEBIDL_FILES = [
|
|||
'FrameLoader.webidl',
|
||||
'HeapSnapshot.webidl',
|
||||
'InspectorUtils.webidl',
|
||||
'IOUtils.webidl',
|
||||
'IteratorResult.webidl',
|
||||
'JSActor.webidl',
|
||||
'JSProcessActor.webidl',
|
||||
|
|
|
@ -0,0 +1,433 @@
|
|||
/* -*- 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 <algorithm>
|
||||
|
||||
#include "mozilla/dom/IOUtils.h"
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/Span.h"
|
||||
#include "nspr/prio.h"
|
||||
#include "nspr/private/pprio.h"
|
||||
#include "nspr/prtypes.h"
|
||||
#include "nsIGlobalObject.h"
|
||||
#include "nsReadableUtils.h"
|
||||
#include "nsString.h"
|
||||
#include "nsThreadManager.h"
|
||||
|
||||
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
|
||||
# include <fcntl.h>
|
||||
#endif
|
||||
|
||||
#define REJECT_IF_NULL_EVENT_TARGET(aEventTarget, aJSPromise) \
|
||||
do { \
|
||||
if (!(aEventTarget)) { \
|
||||
(aJSPromise) \
|
||||
->MaybeRejectWithAbortError( \
|
||||
"Could not dispatch task to background thread"); \
|
||||
return (aJSPromise).forget(); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define REJECT_IF_SHUTTING_DOWN(aJSPromise) \
|
||||
do { \
|
||||
if (sShutdownStarted) { \
|
||||
(aJSPromise) \
|
||||
->MaybeRejectWithNotAllowedError( \
|
||||
"Shutting down and refusing additional I/O tasks"); \
|
||||
return (aJSPromise).forget(); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
|
||||
/* static */
|
||||
StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
|
||||
IOUtils::sBackgroundEventTarget("sBackgroundEventTarget");
|
||||
/* static */
|
||||
StaticRefPtr<nsIAsyncShutdownClient> IOUtils::sBarrier;
|
||||
/* static */
|
||||
Atomic<bool> IOUtils::sShutdownStarted = Atomic<bool>(false);
|
||||
|
||||
/* static */
|
||||
already_AddRefed<Promise> IOUtils::Read(GlobalObject& aGlobal,
|
||||
const nsAString& aPath,
|
||||
const Optional<uint32_t>& aMaxBytes) {
|
||||
RefPtr<Promise> promise = CreateJSPromise(aGlobal);
|
||||
NS_ENSURE_TRUE(!!promise, nullptr);
|
||||
REJECT_IF_SHUTTING_DOWN(promise);
|
||||
|
||||
// Process arguments.
|
||||
uint32_t toRead = 0;
|
||||
if (aMaxBytes.WasPassed()) {
|
||||
toRead = aMaxBytes.Value();
|
||||
if (toRead == 0) {
|
||||
// Resolve with an empty buffer.
|
||||
nsTArray<uint8_t> arr(0);
|
||||
TypedArrayCreator<Uint8Array> arrCreator(arr);
|
||||
promise->MaybeResolve(arrCreator);
|
||||
return promise.forget();
|
||||
}
|
||||
}
|
||||
|
||||
NS_ConvertUTF16toUTF8 path(aPath);
|
||||
|
||||
// Do the IO on a background thread and return the result to this thread.
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
InvokeAsync(
|
||||
bg, __func__,
|
||||
[path, toRead]() {
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
|
||||
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
|
||||
OpenExistingSync(path.get(), PR_RDONLY);
|
||||
if (!fd) {
|
||||
return IOReadMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Could not open file"), __func__);
|
||||
}
|
||||
uint32_t bufSize;
|
||||
if (toRead == 0) { // maxBytes was unspecified.
|
||||
// Limitation: We cannot read files that are larger than the max size
|
||||
// of a TypedArray (UINT32_MAX bytes). Reject if the file is too
|
||||
// big to be read.
|
||||
PRFileInfo64 info;
|
||||
if (PR_FAILURE == PR_GetOpenFileInfo64(fd.get(), &info)) {
|
||||
return IOReadMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Could not get file info"), __func__);
|
||||
}
|
||||
uint32_t fileSize = info.size;
|
||||
if (fileSize > UINT32_MAX) {
|
||||
return IOReadMozPromise::CreateAndReject(
|
||||
nsLiteralCString("File is too large to read"), __func__);
|
||||
}
|
||||
bufSize = fileSize;
|
||||
} else {
|
||||
bufSize = toRead;
|
||||
}
|
||||
nsTArray<uint8_t> fileContents;
|
||||
nsresult er = IOUtils::ReadSync(fd.get(), bufSize, fileContents);
|
||||
|
||||
if (NS_SUCCEEDED(er)) {
|
||||
return IOReadMozPromise::CreateAndResolve(std::move(fileContents),
|
||||
__func__);
|
||||
}
|
||||
if (er == NS_ERROR_OUT_OF_MEMORY) {
|
||||
return IOReadMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Out of memory"), __func__);
|
||||
}
|
||||
return IOReadMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Unexpected error reading file"), __func__);
|
||||
})
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[promise = RefPtr(promise)](const nsTArray<uint8_t>& aBuf) {
|
||||
AutoJSAPI jsapi;
|
||||
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
|
||||
promise->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
const TypedArrayCreator<Uint8Array> arrayCreator(aBuf);
|
||||
promise->MaybeResolve(arrayCreator);
|
||||
},
|
||||
[promise = RefPtr(promise)](const nsACString& aMsg) {
|
||||
promise->MaybeRejectWithOperationError(aMsg);
|
||||
});
|
||||
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<Promise> IOUtils::WriteAtomic(
|
||||
GlobalObject& aGlobal, const nsAString& aPath, const Uint8Array& aData,
|
||||
const WriteAtomicOptions& aOptions) {
|
||||
RefPtr<Promise> promise = CreateJSPromise(aGlobal);
|
||||
NS_ENSURE_TRUE(!!promise, nullptr);
|
||||
REJECT_IF_SHUTTING_DOWN(promise);
|
||||
|
||||
// Process arguments.
|
||||
aData.ComputeState();
|
||||
FallibleTArray<uint8_t> toWrite;
|
||||
if (!toWrite.InsertElementsAt(0, aData.Data(), aData.Length(), fallible)) {
|
||||
promise->MaybeRejectWithOperationError("Out of memory");
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
// TODO: Implement tmpPath and backupFile options.
|
||||
// The data to be written to file might be larger than can be written in any
|
||||
// single call, so we must truncate the file and set the write mode to append
|
||||
// to the file.
|
||||
int32_t flags = PR_WRONLY | PR_TRUNCATE | PR_APPEND;
|
||||
bool noOverwrite = false;
|
||||
if (aOptions.IsAnyMemberPresent()) {
|
||||
if (aOptions.mBackupFile.WasPassed() || aOptions.mTmpPath.WasPassed()) {
|
||||
promise->MaybeRejectWithNotSupportedError(
|
||||
"Unsupported options were passed");
|
||||
return promise.forget();
|
||||
}
|
||||
if (aOptions.mFlush) {
|
||||
flags |= PR_SYNC;
|
||||
}
|
||||
noOverwrite = aOptions.mNoOverwrite;
|
||||
}
|
||||
|
||||
NS_ConvertUTF16toUTF8 path(aPath);
|
||||
|
||||
// Do the IO on a background thread and return the result to this thread.
|
||||
RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
|
||||
REJECT_IF_NULL_EVENT_TARGET(bg, promise);
|
||||
|
||||
InvokeAsync(bg, __func__,
|
||||
[path, flags, noOverwrite, toWrite = std::move(toWrite)]() {
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
|
||||
UniquePtr<PRFileDesc, PR_CloseDelete> fd =
|
||||
OpenExistingSync(path.get(), flags);
|
||||
if (noOverwrite && fd) {
|
||||
return IOWriteMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Refusing to overwrite file"), __func__);
|
||||
}
|
||||
if (!fd) {
|
||||
fd = CreateFileSync(path.get(), flags);
|
||||
}
|
||||
if (!fd) {
|
||||
return IOWriteMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Could not open file"), __func__);
|
||||
}
|
||||
uint32_t result;
|
||||
nsresult er = IOUtils::WriteSync(fd.get(), toWrite, result);
|
||||
|
||||
if (NS_SUCCEEDED(er)) {
|
||||
return IOWriteMozPromise::CreateAndResolve(result, __func__);
|
||||
}
|
||||
return IOWriteMozPromise::CreateAndReject(
|
||||
nsLiteralCString("Unexpected error writing file"),
|
||||
__func__);
|
||||
})
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[promise = RefPtr(promise)](const uint32_t& aBytesWritten) {
|
||||
AutoJSAPI jsapi;
|
||||
if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
|
||||
promise->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
promise->MaybeResolve(aBytesWritten);
|
||||
},
|
||||
[promise = RefPtr(promise)](const nsACString& aMsg) {
|
||||
promise->MaybeRejectWithOperationError(aMsg);
|
||||
});
|
||||
|
||||
return promise.forget();
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<nsISerialEventTarget> IOUtils::GetBackgroundEventTarget() {
|
||||
if (sShutdownStarted) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto lockedBackgroundEventTarget = sBackgroundEventTarget.Lock();
|
||||
if (!lockedBackgroundEventTarget.ref()) {
|
||||
RefPtr<nsISerialEventTarget> et;
|
||||
MOZ_ALWAYS_SUCCEEDS(NS_CreateBackgroundTaskQueue(
|
||||
"IOUtils::BackgroundIOThread", getter_AddRefs(et)));
|
||||
MOZ_ASSERT(et);
|
||||
*lockedBackgroundEventTarget = et;
|
||||
|
||||
if (NS_IsMainThread()) {
|
||||
IOUtils::SetShutdownHooks();
|
||||
} else {
|
||||
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
|
||||
__func__, []() { IOUtils::SetShutdownHooks(); });
|
||||
NS_DispatchToMainThread(runnable.forget());
|
||||
}
|
||||
}
|
||||
return do_AddRef(*lockedBackgroundEventTarget);
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<nsIAsyncShutdownClient> IOUtils::GetShutdownBarrier() {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread());
|
||||
|
||||
if (!sBarrier) {
|
||||
nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
|
||||
MOZ_ASSERT(svc);
|
||||
|
||||
nsCOMPtr<nsIAsyncShutdownClient> barrier;
|
||||
nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
|
||||
NS_ENSURE_SUCCESS(rv, nullptr);
|
||||
sBarrier = barrier;
|
||||
}
|
||||
return do_AddRef(sBarrier);
|
||||
}
|
||||
|
||||
/* static */
|
||||
void IOUtils::SetShutdownHooks() {
|
||||
MOZ_RELEASE_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
|
||||
nsCOMPtr<nsIAsyncShutdownBlocker> blocker = new IOUtilsShutdownBlocker();
|
||||
|
||||
nsresult rv = barrier->AddBlocker(
|
||||
blocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
|
||||
u"IOUtils: waiting for pending I/O to finish"_ns);
|
||||
// Adding a new shutdown blocker should only fail if the current shutdown
|
||||
// phase has completed. Ensure that we have set our shutdown flag to stop
|
||||
// accepting new I/O tasks in this case.
|
||||
if (NS_FAILED(rv)) {
|
||||
sShutdownStarted = true;
|
||||
}
|
||||
NS_ENSURE_SUCCESS_VOID(rv);
|
||||
}
|
||||
|
||||
/* static */
|
||||
already_AddRefed<Promise> IOUtils::CreateJSPromise(GlobalObject& aGlobal) {
|
||||
ErrorResult er;
|
||||
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
|
||||
RefPtr<Promise> promise = Promise::Create(global, er);
|
||||
if (er.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
MOZ_ASSERT(promise);
|
||||
return do_AddRef(promise);
|
||||
}
|
||||
|
||||
/* static */
|
||||
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::OpenExistingSync(
|
||||
const char* aPath, int32_t aFlags) {
|
||||
// Ensure that CREATE_FILE and EXCL flags were not included, as we do not
|
||||
// want to create a new file.
|
||||
MOZ_ASSERT((aFlags & (PR_CREATE_FILE | PR_EXCL)) == 0);
|
||||
|
||||
return UniquePtr<PRFileDesc, PR_CloseDelete>(PR_Open(aPath, aFlags, 0));
|
||||
}
|
||||
|
||||
/* static */
|
||||
UniquePtr<PRFileDesc, PR_CloseDelete> IOUtils::CreateFileSync(const char* aPath,
|
||||
int32_t aFlags,
|
||||
int32_t aMode) {
|
||||
return UniquePtr<PRFileDesc, PR_CloseDelete>(
|
||||
PR_Open(aPath, aFlags | PR_CREATE_FILE | PR_EXCL, aMode));
|
||||
}
|
||||
|
||||
/* static */
|
||||
nsresult IOUtils::ReadSync(PRFileDesc* aFd, const uint32_t aBufSize,
|
||||
nsTArray<uint8_t>& aResult) {
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
|
||||
nsTArray<uint8_t> buffer;
|
||||
if (!buffer.SetCapacity(aBufSize, fallible)) {
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// If possible, advise the operating system that we will be reading the file
|
||||
// pointed to by |aFD| sequentially, in full. This advice is not binding, it
|
||||
// informs the OS about our expectations as an application.
|
||||
#if defined(HAVE_POSIX_FADVISE)
|
||||
posix_fadvise(PR_FileDesc2NativeHandle(aFd), 0, 0, POSIX_FADV_SEQUENTIAL);
|
||||
#endif
|
||||
|
||||
uint32_t totalRead = 0;
|
||||
while (totalRead != aBufSize) {
|
||||
int32_t nRead =
|
||||
PR_Read(aFd, buffer.Elements() + totalRead, aBufSize - totalRead);
|
||||
if (nRead == 0) {
|
||||
break;
|
||||
}
|
||||
if (nRead < 0) {
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
totalRead += nRead;
|
||||
DebugOnly<bool> success = buffer.SetLength(totalRead, fallible);
|
||||
MOZ_ASSERT(success);
|
||||
}
|
||||
aResult = std::move(buffer);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
nsresult IOUtils::WriteSync(PRFileDesc* aFd, const nsTArray<uint8_t>& aBytes,
|
||||
uint32_t& aResult) {
|
||||
// aBytes comes from a JavaScript TypedArray, which has UINT32_MAX max length.
|
||||
MOZ_ASSERT(aBytes.Length() <= UINT32_MAX);
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
|
||||
if (aBytes.Length() == 0) {
|
||||
aResult = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
uint32_t bytesWritten = 0;
|
||||
|
||||
// PR_Write can only write up to PR_INT32_MAX bytes at a time, but the data
|
||||
// source might be as large as UINT32_MAX bytes.
|
||||
uint32_t chunkSize = PR_INT32_MAX;
|
||||
uint32_t pendingBytes = aBytes.Length();
|
||||
|
||||
while (pendingBytes > 0) {
|
||||
if (pendingBytes < chunkSize) {
|
||||
chunkSize = pendingBytes;
|
||||
}
|
||||
int32_t rv = PR_Write(aFd, aBytes.Elements() + bytesWritten, chunkSize);
|
||||
if (rv < 0) {
|
||||
return NS_ERROR_FILE_CORRUPTED;
|
||||
}
|
||||
pendingBytes -= rv;
|
||||
bytesWritten += rv;
|
||||
}
|
||||
|
||||
aResult = bytesWritten;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker);
|
||||
|
||||
NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
|
||||
aName = u"IOUtils Blocker"_ns;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
|
||||
nsIAsyncShutdownClient* aBarrierClient) {
|
||||
nsCOMPtr<nsISerialEventTarget> et = IOUtils::GetBackgroundEventTarget();
|
||||
|
||||
IOUtils::sShutdownStarted = true;
|
||||
|
||||
if (!IOUtils::sBarrier) {
|
||||
return NS_ERROR_NULL_POINTER;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIRunnable> backgroundRunnable =
|
||||
NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() {
|
||||
nsCOMPtr<nsIRunnable> mainThreadRunnable =
|
||||
NS_NewRunnableFunction(__func__, [self = RefPtr(self)]() {
|
||||
IOUtils::sBarrier->RemoveBlocker(self);
|
||||
|
||||
auto lockedBackgroundET = IOUtils::sBackgroundEventTarget.Lock();
|
||||
*lockedBackgroundET = nullptr;
|
||||
IOUtils::sBarrier = nullptr;
|
||||
});
|
||||
nsresult er = NS_DispatchToMainThread(mainThreadRunnable.forget());
|
||||
NS_ENSURE_SUCCESS_VOID(er);
|
||||
});
|
||||
|
||||
return et->Dispatch(backgroundRunnable.forget(),
|
||||
nsIEventTarget::DISPATCH_NORMAL);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP IOUtilsShutdownBlocker::GetState(nsIPropertyBag** aState) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#undef REJECT_IF_NULL_EVENT_TARGET
|
||||
#undef REJECT_IF_SHUTTING_DOWN
|
|
@ -7,7 +7,16 @@
|
|||
#ifndef mozilla_dom_IOUtils__
|
||||
#define mozilla_dom_IOUtils__
|
||||
|
||||
#include <prio.h>
|
||||
#include "mozilla/AlreadyAddRefed.h"
|
||||
#include "mozilla/DataMutex.h"
|
||||
#include "mozilla/dom/BindingDeclarations.h"
|
||||
#include "mozilla/dom/IOUtilsBinding.h"
|
||||
#include "mozilla/dom/TypedArray.h"
|
||||
#include "mozilla/ErrorResult.h"
|
||||
#include "mozilla/MozPromise.h"
|
||||
#include "nspr/prio.h"
|
||||
#include "nsIAsyncShutdown.h"
|
||||
#include "nsISerialEventTarget.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
|
@ -30,6 +39,81 @@ class PR_CloseDelete {
|
|||
void operator()(PRFileDesc* aPtr) const { PR_Close(aPtr); }
|
||||
};
|
||||
|
||||
namespace dom {
|
||||
|
||||
class IOUtils final {
|
||||
public:
|
||||
static already_AddRefed<Promise> Read(GlobalObject& aGlobal,
|
||||
const nsAString& aPath,
|
||||
const Optional<uint32_t>& aMaxBytes);
|
||||
|
||||
static already_AddRefed<Promise> WriteAtomic(
|
||||
GlobalObject& aGlobal, const nsAString& aPath, const Uint8Array& aData,
|
||||
const WriteAtomicOptions& aOptions);
|
||||
|
||||
private:
|
||||
~IOUtils() = default;
|
||||
|
||||
friend class IOUtilsShutdownBlocker;
|
||||
|
||||
static StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
|
||||
sBackgroundEventTarget;
|
||||
static StaticRefPtr<nsIAsyncShutdownClient> sBarrier;
|
||||
static Atomic<bool> sShutdownStarted;
|
||||
|
||||
static already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier();
|
||||
|
||||
static already_AddRefed<nsISerialEventTarget> GetBackgroundEventTarget();
|
||||
|
||||
static void SetShutdownHooks();
|
||||
|
||||
static already_AddRefed<Promise> CreateJSPromise(GlobalObject& aGlobal);
|
||||
|
||||
/**
|
||||
* Opens an existing file at |path|.
|
||||
*
|
||||
* @param path The location of the file as a unix-style UTF-8 path string.
|
||||
* @param flags PRIO flags, excluding |PR_CREATE| and |PR_EXCL|.
|
||||
*/
|
||||
static UniquePtr<PRFileDesc, PR_CloseDelete> OpenExistingSync(
|
||||
const char* aPath, int32_t aFlags);
|
||||
|
||||
/**
|
||||
* Creates a new file at |path|.
|
||||
*
|
||||
* @param aPath The location of the file as a unix-style UTF-8 path string.
|
||||
* @param aFlags PRIO flags to be used in addition to |PR_CREATE| and
|
||||
* |PR_EXCL|.
|
||||
* @param aMode Optional file mode. Defaults to 0666 to allow the system
|
||||
* umask to compute the best mode for the new file.
|
||||
*/
|
||||
static UniquePtr<PRFileDesc, PR_CloseDelete> CreateFileSync(
|
||||
const char* aPath, int32_t aFlags, int32_t aMode = 0666);
|
||||
|
||||
static nsresult ReadSync(PRFileDesc* aFd, const uint32_t aBufSize,
|
||||
nsTArray<uint8_t>& aResult);
|
||||
|
||||
static nsresult WriteSync(PRFileDesc* aFd, const nsTArray<uint8_t>& aBytes,
|
||||
uint32_t& aResult);
|
||||
|
||||
using IOReadMozPromise =
|
||||
mozilla::MozPromise<nsTArray<uint8_t>, const nsCString,
|
||||
/* IsExclusive */ true>;
|
||||
|
||||
using IOWriteMozPromise =
|
||||
mozilla::MozPromise<uint32_t, const nsCString, /* IsExclusive */ true>;
|
||||
};
|
||||
|
||||
class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIASYNCSHUTDOWNBLOCKER
|
||||
|
||||
private:
|
||||
virtual ~IOUtilsShutdownBlocker() = default;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
} // namespace mozilla
|
||||
|
||||
#endif
|
||||
|
|
|
@ -78,6 +78,7 @@ EXPORTS.mozilla.dom += [
|
|||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'IOUtils.cpp',
|
||||
'nsDeviceSensors.cpp',
|
||||
'nsOSPermissionRequestBase.cpp',
|
||||
'OSFileConstants.cpp',
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
[DEFAULT]
|
||||
support-files = worker_constants.js
|
||||
support-files =
|
||||
worker_constants.js
|
||||
file_ioutils_worker.js
|
||||
|
||||
[test_constants.xhtml]
|
||||
[test_ioutils.html]
|
||||
[test_ioutils_worker.xhtml]
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
// Any copyright is dedicated to the Public Domain.
|
||||
// - http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
/* eslint-env mozilla/chrome-worker, node */
|
||||
/* global finish, log*/
|
||||
|
||||
"use strict";
|
||||
|
||||
importScripts("chrome://mochikit/content/tests/SimpleTest/WorkerSimpleTest.js");
|
||||
importScripts("resource://gre/modules/ObjectUtils.jsm");
|
||||
|
||||
// TODO: Remove this import for OS.File. It is currently being used as a
|
||||
// stop gap for missing IOUtils functionality.
|
||||
importScripts("resource://gre/modules/osfile.jsm");
|
||||
|
||||
self.onmessage = async function(msg) {
|
||||
// IOUtils functionality is the same when called from the main thread, or a
|
||||
// web worker. These tests are a modified subset of the main thread tests, and
|
||||
// serve as a sanity check that the implementation is thread-safe.
|
||||
await test_api_is_available_on_worker();
|
||||
await test_full_read_and_write();
|
||||
|
||||
finish();
|
||||
info("test_ioutils_worker.xhtml: Test finished");
|
||||
|
||||
async function test_api_is_available_on_worker() {
|
||||
ok(self.IOUtils, "IOUtils is present in web workers");
|
||||
}
|
||||
|
||||
async function test_full_read_and_write() {
|
||||
// Write a file.
|
||||
const tmpFileName = "test_ioutils_numbers.tmp";
|
||||
const bytes = Uint8Array.of(...new Array(50).keys());
|
||||
const bytesWritten = await self.IOUtils.writeAtomic(tmpFileName, bytes);
|
||||
is(
|
||||
bytesWritten,
|
||||
50,
|
||||
"IOUtils::writeAtomic can write entire byte array to file"
|
||||
);
|
||||
|
||||
// Read it back.
|
||||
let fileContents = await self.IOUtils.read(tmpFileName);
|
||||
ok(
|
||||
ObjectUtils.deepEqual(bytes, fileContents) &&
|
||||
bytes.length == fileContents.length,
|
||||
"IOUtils::read can read back entire file"
|
||||
);
|
||||
|
||||
const tooManyBytes = bytes.length + 1;
|
||||
fileContents = await self.IOUtils.read(tmpFileName, tooManyBytes);
|
||||
ok(
|
||||
ObjectUtils.deepEqual(bytes, fileContents) &&
|
||||
fileContents.length == bytes.length,
|
||||
"IOUtils::read can read entire file when requested maxBytes is too large"
|
||||
);
|
||||
|
||||
cleanup(tmpFileName);
|
||||
}
|
||||
};
|
||||
|
||||
function cleanup(...files) {
|
||||
files.forEach(file => {
|
||||
OS.File.remove(file);
|
||||
ok(!OS.File.exists(file), `Removed temporary file: ${file}`);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
- http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the IOUtils file I/O API</title>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<!---
|
||||
This implementation is compared against an already well-tested reference
|
||||
implementation of File I/0.
|
||||
-->
|
||||
<script src="resource://gre/modules/FileTestUtils.jsm"></script>
|
||||
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
|
||||
const { ObjectUtils } = ChromeUtils.import("resource://gre/modules/ObjectUtils.jsm");
|
||||
|
||||
// TODO: Remove this import for OS.File. It is currently being used as a
|
||||
// stop gap for missing IOUtils functionality.
|
||||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
|
||||
|
||||
add_task(async function test_api_is_available_on_window() {
|
||||
ok(window.IOUtils, "IOUtils is present on the window");
|
||||
});
|
||||
|
||||
add_task(async function test_read_failure() {
|
||||
await Assert.rejects(
|
||||
window.IOUtils.read("does_not_exist.txt"),
|
||||
/Could not open file/,
|
||||
"IOUtils::read rejects when file does not exist"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_write_no_overwrite() {
|
||||
// Make a new file, and try to write to it with overwrites disabled.
|
||||
const tmpFileName = "test_ioutils_overwrite.tmp";
|
||||
const untouchableContents = new TextEncoder().encode("Can't touch this!\n");
|
||||
await window.IOUtils.writeAtomic(tmpFileName, untouchableContents);
|
||||
|
||||
const newContents = new TextEncoder().encode("Nah nah nah!\n");
|
||||
await Assert.rejects(
|
||||
window.IOUtils.writeAtomic(tmpFileName, newContents, {
|
||||
noOverwrite: true,
|
||||
}),
|
||||
/Refusing to overwrite file/,
|
||||
"IOUtils::writeAtomic rejects writing to existing file if overwrites are disabled"
|
||||
);
|
||||
|
||||
const bytesWritten = await window.IOUtils.writeAtomic(
|
||||
tmpFileName,
|
||||
newContents,
|
||||
{ noOverwrite: false /* Default. */ }
|
||||
);
|
||||
is(
|
||||
bytesWritten,
|
||||
newContents.length,
|
||||
"IOUtils::writeAtomic can overwrite files if specified"
|
||||
);
|
||||
|
||||
await cleanup(tmpFileName);
|
||||
});
|
||||
|
||||
add_task(async function test_partial_read() {
|
||||
const tmpFileName = "test_ioutils_partial_read.tmp";
|
||||
const bytes = Uint8Array.of(...new Array(50).keys());
|
||||
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
|
||||
is(
|
||||
bytesWritten,
|
||||
50,
|
||||
"IOUtils::writeAtomic can write entire byte array to file"
|
||||
);
|
||||
|
||||
// Read just the first 10 bytes.
|
||||
const first10 = bytes.slice(0, 10);
|
||||
const bytes10 = await window.IOUtils.read(tmpFileName, 10);
|
||||
ok(
|
||||
ObjectUtils.deepEqual(bytes10, first10),
|
||||
"IOUtils::read can read part of a file, up to specified max bytes"
|
||||
);
|
||||
|
||||
// Trying to explicitly read nothing isn't useful, but it should still
|
||||
// succeed.
|
||||
const bytes0 = await window.IOUtils.read(tmpFileName, 0);
|
||||
is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
|
||||
|
||||
await cleanup(tmpFileName);
|
||||
});
|
||||
|
||||
add_task(async function test_empty_read_and_write() {
|
||||
// Trying to write an empty file isn't very useful, but it should still
|
||||
// succeed.
|
||||
const tmpFileName = "test_ioutils_empty.tmp";
|
||||
const emptyByteArray = new Uint8Array(0);
|
||||
const bytesWritten = await window.IOUtils.writeAtomic(
|
||||
tmpFileName,
|
||||
emptyByteArray
|
||||
);
|
||||
is(bytesWritten, 0, "IOUtils::writeAtomic can create an empty file");
|
||||
|
||||
// Trying to explicitly read nothing isn't useful, but it should still
|
||||
// succeed.
|
||||
const bytes0 = await window.IOUtils.read(tmpFileName, 0);
|
||||
is(bytes0.length, 0, "IOUtils::read can read 0 bytes");
|
||||
|
||||
// Implicitly try to read nothing.
|
||||
const nothing = await window.IOUtils.read(tmpFileName);
|
||||
is(nothing.length, 0, "IOUtils:: read can read empty files");
|
||||
|
||||
await cleanup(tmpFileName);
|
||||
});
|
||||
|
||||
add_task(async function test_full_read_and_write() {
|
||||
// Write a file.
|
||||
const tmpFileName = "test_ioutils_numbers.tmp";
|
||||
const bytes = Uint8Array.of(...new Array(50).keys());
|
||||
const bytesWritten = await window.IOUtils.writeAtomic(tmpFileName, bytes);
|
||||
is(
|
||||
bytesWritten,
|
||||
50,
|
||||
"IOUtils::writeAtomic can write entire byte array to file"
|
||||
);
|
||||
|
||||
// Read it back.
|
||||
let fileContents = await window.IOUtils.read(tmpFileName);
|
||||
ok(
|
||||
ObjectUtils.deepEqual(bytes, fileContents) &&
|
||||
bytes.length == fileContents.length,
|
||||
"IOUtils::read can read back entire file"
|
||||
);
|
||||
|
||||
const tooManyBytes = bytes.length + 1;
|
||||
fileContents = await window.IOUtils.read(tmpFileName, tooManyBytes);
|
||||
ok(
|
||||
ObjectUtils.deepEqual(bytes, fileContents) &&
|
||||
fileContents.length == bytes.length,
|
||||
"IOUtils::read can read entire file when requested maxBytes is too large"
|
||||
);
|
||||
|
||||
await cleanup(tmpFileName);
|
||||
});
|
||||
|
||||
|
||||
// Utility functions.
|
||||
async function cleanup(...files) {
|
||||
for (const file of files) {
|
||||
await OS.File.remove(file);
|
||||
const exists = await OS.File.exists(file);
|
||||
ok(!exists, `Removed temporary file: ${file}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
-->
|
||||
<window title="Testing IOUtils on a chrome worker thread"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="test();">
|
||||
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
|
||||
<script src="chrome://mochikit/content/tests/SimpleTest/WorkerHandler.js"/>
|
||||
|
||||
<script type="application/javascript">
|
||||
<![CDATA[
|
||||
|
||||
// Test IOUtils in a chrome worker.
|
||||
function test() {
|
||||
// finish() will be called in the worker.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
info("test_ioutils_worker.xhtml: Starting test");
|
||||
|
||||
const worker = new ChromeWorker("file_ioutils_worker.js");
|
||||
info("test_ioutils_worker.xhtml: Chrome worker created");
|
||||
|
||||
// Set up the worker with testing facilities, and start it.
|
||||
listenForTests(worker, { verbose: false });
|
||||
worker.postMessage(0);
|
||||
info("test_ioutils_worker.xhtml: Test in progress");
|
||||
};
|
||||
|
||||
]]>
|
||||
</script>
|
||||
|
||||
<body xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display:none;"></div>
|
||||
<pre id="test"></pre>
|
||||
</body>
|
||||
<label id="test-result" />
|
||||
</window>
|
Загрузка…
Ссылка в новой задаче