Bug 901097 - FileReader API in workers, r=sicking, r=nfroyd

This commit is contained in:
Andrea Marchesini 2015-12-16 12:37:31 +00:00
parent 36fe774c56
commit ac410a8dd0
13 changed files with 786 additions and 199 deletions

View File

@ -6,34 +6,34 @@
#include "FileReader.h"
#include "nsContentCID.h"
#include "nsContentUtils.h"
#include "nsDOMClassInfoID.h"
#include "nsError.h"
#include "nsIFile.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsIEventTarget.h"
#include "nsIGlobalObject.h"
#include "nsITimer.h"
#include "nsITransport.h"
#include "nsIStreamTransportService.h"
#include "nsXPCOM.h"
#include "nsIDOMEventListener.h"
#include "nsJSEnvironment.h"
#include "nsCycleCollectionParticipant.h"
#include "mozilla/Base64.h"
#include "mozilla/dom/DOMError.h"
#include "mozilla/dom/EncodingUtils.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/FileReaderBinding.h"
#include "mozilla/dom/ProgressEvent.h"
#include "xpcpublic.h"
#include "nsContentUtils.h"
#include "nsCycleCollectionParticipant.h"
#include "nsDOMJSUtils.h"
#include "nsError.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "xpcpublic.h"
#include "jsfriendapi.h"
#include "nsITransport.h"
#include "nsIStreamTransportService.h"
#include "WorkerPrivate.h"
#include "WorkerScope.h"
namespace mozilla {
namespace dom {
using namespace workers;
#define ABORT_STR "abort"
#define LOAD_STR "load"
#define LOADSTART_STR "loadstart"
@ -76,6 +76,20 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
NS_IMPL_ADDREF_INHERITED(FileReader, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(FileReader, DOMEventTargetHelper)
class MOZ_RAII FileReaderDecreaseBusyCounter
{
RefPtr<FileReader> mFileReader;
public:
explicit FileReaderDecreaseBusyCounter(FileReader* aFileReader)
: mFileReader(aFileReader)
{}
~FileReaderDecreaseBusyCounter()
{
mFileReader->DecreaseBusyCounter();
}
};
void
FileReader::RootResultArrayBuffer()
{
@ -84,7 +98,8 @@ FileReader::RootResultArrayBuffer()
//FileReader constructors/initializers
FileReader::FileReader(nsPIDOMWindow* aWindow)
FileReader::FileReader(nsPIDOMWindow* aWindow,
WorkerPrivate* aWorkerPrivate)
: DOMEventTargetHelper(aWindow)
, mFileData(nullptr)
, mDataLen(0)
@ -95,7 +110,12 @@ FileReader::FileReader(nsPIDOMWindow* aWindow)
, mReadyState(EMPTY)
, mTotal(0)
, mTransferred(0)
, mTarget(do_GetCurrentThread())
, mBusyCount(0)
, mWorkerPrivate(aWorkerPrivate)
{
MOZ_ASSERT_IF(!NS_IsMainThread(), mWorkerPrivate && !aWindow);
MOZ_ASSERT_IF(NS_IsMainThread(), !mWorkerPrivate);
SetDOMStringToNull(mResult);
}
@ -111,9 +131,17 @@ FileReader::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv)
{
// The owner can be null when this object is used by chrome code.
nsCOMPtr<nsPIDOMWindow> owner = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<FileReader> fileReader = new FileReader(owner);
WorkerPrivate* workerPrivate = nullptr;
if (!owner && nsContentUtils::IsCallerChrome()) {
if (!NS_IsMainThread()) {
JSContext* cx = aGlobal.Context();
workerPrivate = GetWorkerPrivateFromContext(cx);
MOZ_ASSERT(workerPrivate);
}
RefPtr<FileReader> fileReader = new FileReader(owner, workerPrivate);
if (!owner && nsContentUtils::ThreadsafeIsCallerChrome()) {
// Instead of grabbing some random global from the context stack,
// let's use the default one (junk scope) for now.
// We should move away from this Init...
@ -215,7 +243,17 @@ FileReader::DoOnLoadEnd(nsresult aStatus,
switch (mDataFormat) {
case FILE_AS_ARRAYBUFFER: {
AutoJSAPI jsapi;
if (NS_WARN_IF(!jsapi.Init(DOMEventTargetHelper::GetParentObject()))) {
nsCOMPtr<nsIGlobalObject> globalObject;
if (NS_IsMainThread()) {
globalObject = do_QueryInterface(GetParentObject());
} else {
MOZ_ASSERT(mWorkerPrivate);
MOZ_ASSERT(mBusyCount);
globalObject = mWorkerPrivate->GlobalScope();
}
if (!globalObject || !jsapi.Init(globalObject)) {
FreeFileData();
return NS_ERROR_FAILURE;
}
@ -256,9 +294,29 @@ FileReader::DoOnLoadEnd(nsresult aStatus,
}
nsresult
FileReader::DoReadData(nsIAsyncInputStream* aStream, uint64_t aCount)
FileReader::DoAsyncWait()
{
MOZ_ASSERT(aStream);
nsresult rv = IncreaseBusyCounter();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = mAsyncStream->AsyncWait(this,
/* aFlags*/ 0,
/* aRequestedCount */ 0,
mTarget);
if (NS_WARN_IF(NS_FAILED(rv))) {
DecreaseBusyCounter();
return rv;
}
return NS_OK;
}
nsresult
FileReader::DoReadData(uint64_t aCount)
{
MOZ_ASSERT(mAsyncStream);
if (mDataFormat == FILE_AS_BINARY) {
//Continuously update our binary string as data comes in
@ -271,8 +329,8 @@ FileReader::DoReadData(nsIAsyncInputStream* aStream, uint64_t aCount)
NS_ENSURE_TRUE(buf, NS_ERROR_OUT_OF_MEMORY);
uint32_t bytesRead = 0;
aStream->ReadSegments(ReadFuncBinaryString, buf + oldLen, aCount,
&bytesRead);
mAsyncStream->ReadSegments(ReadFuncBinaryString, buf + oldLen, aCount,
&bytesRead);
NS_ASSERTION(bytesRead == aCount, "failed to read data");
}
else {
@ -287,7 +345,7 @@ FileReader::DoReadData(nsIAsyncInputStream* aStream, uint64_t aCount)
}
uint32_t bytesRead = 0;
aStream->Read(mFileData + mDataLen, aCount, &bytesRead);
mAsyncStream->Read(mFileData + mDataLen, aCount, &bytesRead);
NS_ASSERTION(bytesRead == aCount, "failed to read data");
}
@ -361,10 +419,7 @@ FileReader::ReadFileContent(Blob& aBlob,
return;
}
aRv = mAsyncStream->AsyncWait(this,
/* aFlags*/ 0,
/* aRequestedCount */ 0,
NS_GetCurrentThread());
aRv = DoAsyncWait();
if (NS_WARN_IF(aRv.Failed())) {
return;
}
@ -467,6 +522,7 @@ FileReader::StartProgressEventTimer()
mProgressEventWasDelayed = false;
mTimerIsActive = true;
mProgressNotifier->Cancel();
mProgressNotifier->SetTarget(mTarget);
mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL,
nsITimer::TYPE_ONE_SHOT);
}
@ -550,18 +606,20 @@ FileReader::OnInputStreamReady(nsIAsyncInputStream* aStream)
return NS_OK;
}
// We use this class to decrease the busy counter at the end of this method.
// In theory we can do it immediatelly but, for debugging reasons, we want to
// be 100% sure we have a feature when OnLoadEnd() is called.
FileReaderDecreaseBusyCounter RAII(this);
uint64_t aCount;
nsresult rv = aStream->Available(&aCount);
if (NS_SUCCEEDED(rv) && aCount) {
rv = DoReadData(aStream, aCount);
rv = DoReadData(aCount);
}
if (NS_SUCCEEDED(rv)) {
rv = aStream->AsyncWait(this,
/* aFlags*/ 0,
/* aRequestedCount */ 0,
NS_GetCurrentThread());
rv = DoAsyncWait();
}
if (NS_FAILED(rv) || !aCount) {
@ -643,5 +701,44 @@ FileReader::Abort(ErrorResult& aRv)
DispatchProgressEvent(NS_LITERAL_STRING(LOADEND_STR));
}
nsresult
FileReader::IncreaseBusyCounter()
{
if (mWorkerPrivate && mBusyCount++ == 0 &&
!mWorkerPrivate->AddFeature(mWorkerPrivate->GetJSContext(), this)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
void
FileReader::DecreaseBusyCounter()
{
MOZ_ASSERT_IF(mWorkerPrivate, mBusyCount);
if (mWorkerPrivate && --mBusyCount == 0) {
mWorkerPrivate->RemoveFeature(mWorkerPrivate->GetJSContext(), this);
}
}
bool
FileReader::Notify(JSContext* aCx, Status aStatus)
{
MOZ_ASSERT(mWorkerPrivate);
mWorkerPrivate->AssertIsOnWorkerThread();
if (aStatus > Running) {
if (mAsyncStream) {
mAsyncStream->Close();
mAsyncStream = nullptr;
}
mWorkerPrivate->RemoveFeature(mWorkerPrivate->GetJSContext(), this);
mWorkerPrivate = nullptr;
}
return true;
}
} // dom namespace
} // mozilla namespace

View File

@ -9,37 +9,45 @@
#include "mozilla/Attributes.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/dom/DOMError.h"
#include "nsCOMPtr.h"
#include "nsIAsyncInputStream.h"
#include "nsIStreamListener.h"
#include "nsISupportsUtils.h"
#include "nsIInterfaceRequestor.h"
#include "nsITimer.h"
#include "nsJSUtils.h"
#include "nsCOMPtr.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nsWeakReference.h"
#include "prtime.h"
#include "WorkerFeature.h"
#define NS_PROGRESS_EVENT_INTERVAL 50
class nsITimer;
class nsIEventTarget;
namespace mozilla {
namespace dom {
class Blob;
class DOMError;
namespace workers {
class WorkerPrivate;
}
extern const uint64_t kUnknownSize;
class FileReaderDecreaseBusyCounter;
class FileReader final : public DOMEventTargetHelper,
public nsIInterfaceRequestor,
public nsSupportsWeakReference,
public nsIInputStreamCallback,
public nsITimerCallback
public nsITimerCallback,
public workers::WorkerFeature
{
friend class FileReaderDecreaseBusyCounter;
public:
explicit FileReader(nsPIDOMWindow* aWindow);
FileReader(nsPIDOMWindow* aWindow,
workers::WorkerPrivate* aWorkerPrivate);
NS_DECL_ISUPPORTS_INHERITED
@ -47,9 +55,11 @@ public:
NS_DECL_NSIINPUTSTREAMCALLBACK
NS_DECL_NSIINTERFACEREQUESTOR
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(FileReader, DOMEventTargetHelper)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(FileReader,
DOMEventTargetHelper)
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
virtual JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
// WebIDL
static already_AddRefed<FileReader>
@ -96,6 +106,9 @@ public:
ReadFileContent(aBlob, EmptyString(), FILE_AS_BINARY, aRv);
}
// WorkerFeature
bool Notify(JSContext* aCx, workers::Status) override;
private:
virtual ~FileReader();
@ -131,7 +144,8 @@ private:
void DispatchError(nsresult rv, nsAString& finalEvent);
nsresult DispatchProgressEvent(const nsAString& aType);
nsresult DoReadData(nsIAsyncInputStream* aStream, uint64_t aCount);
nsresult DoAsyncWait();
nsresult DoReadData(uint64_t aCount);
nsresult DoOnLoadEnd(nsresult aStatus, nsAString& aSuccessEvent,
nsAString& aTerminationEvent);
@ -143,6 +157,9 @@ private:
mDataLen = 0;
}
nsresult IncreaseBusyCounter();
void DecreaseBusyCounter();
char *mFileData;
RefPtr<Blob> mBlob;
nsCString mCharset;
@ -166,6 +183,13 @@ private:
uint64_t mTotal;
uint64_t mTransferred;
nsCOMPtr<nsIEventTarget> mTarget;
uint64_t mBusyCount;
// Kept alive with a WorkerFeature.
workers::WorkerPrivate* mWorkerPrivate;
};
} // dom namespace

View File

@ -11,7 +11,7 @@
*/
[Constructor,
Exposed=(Window,System)]
Exposed=(Window,Worker,System)]
interface FileReader : EventTarget {
// async read methods
[Throws]

View File

@ -0,0 +1,29 @@
var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.importGlobalProperties(["File"]);
var fileNum = 1;
function createFileWithData(fileData) {
var willDelete = fileData === null;
var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
var testFile = dirSvc.get("ProfD", Ci.nsIFile);
testFile.append("fileAPItestfile" + fileNum);
fileNum++;
var outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate
0666, 0);
if (willDelete) {
fileData = "some irrelevant test data\n";
}
outStream.write(fileData, fileData.length);
outStream.close();
var domFile = new File(testFile);
if (willDelete) {
testFile.remove(/* recursive: */ false);
}
return domFile;
}
addMessageListener("files.open", function (message) {
sendAsyncMessage("files.opened", message.map(createFileWithData));
});

View File

@ -119,6 +119,8 @@ support-files =
worker_referrer.js
websocket_https.html
websocket_https_worker.js
worker_fileReader.js
fileapi_chromeScript.js
[test_404.html]
[test_atob.html]
@ -236,3 +238,4 @@ skip-if = (os == "win") || (os == "mac") || toolkit == 'android' #bug 798220
[test_referrer.html]
[test_sharedWorker_ports.html]
[test_sharedWorker_lifetime.html]
[test_fileReader.html]

View File

@ -114,6 +114,8 @@ var interfaceNamesInGlobalScope =
"FetchEvent",
// IMPORTANT: Do not change this list without review from a DOM peer!
"File",
// IMPORTANT: Do not change this list without review from a DOM peer!
"FileReader",
// IMPORTANT: Do not change this list without review from a DOM peer!
"FileReaderSync",
// IMPORTANT: Do not change this list without review from a DOM peer!

View File

@ -0,0 +1,100 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for FileReader in workers</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script type="text/javascript;version=1.7">
const minFileSize = 20000;
SimpleTest.waitForExplicitFinish();
// Create strings containing data we'll test with. We'll want long
// strings to ensure they span multiple buffers while loading
var testTextData = "asd b\tlah\u1234w\u00a0r";
while (testTextData.length < minFileSize) {
testTextData = testTextData + testTextData;
}
var testASCIIData = "abcdef 123456\n";
while (testASCIIData.length < minFileSize) {
testASCIIData = testASCIIData + testASCIIData;
}
var testBinaryData = "";
for (var i = 0; i < 256; i++) {
testBinaryData += String.fromCharCode(i);
}
while (testBinaryData.length < minFileSize) {
testBinaryData = testBinaryData + testBinaryData;
}
var dataurldata0 = testBinaryData.substr(0, testBinaryData.length -
testBinaryData.length % 3);
var dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 -
testBinaryData.length % 3);
var dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 -
testBinaryData.length % 3);
//Set up files for testing
var openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
var opener = SpecialPowers.loadChromeScript(openerURL);
opener.addMessageListener("files.opened", onFilesOpened);
opener.sendAsyncMessage("files.open", [
testASCIIData,
testBinaryData,
null,
convertToUTF8(testTextData),
convertToUTF16(testTextData),
"",
dataurldata0,
dataurldata1,
dataurldata2,
]);
function onFilesOpened(message) {
var worker = new Worker('worker_fileReader.js');
worker.postMessage({ blobs: message,
testTextData: testTextData,
testASCIIData: testASCIIData,
testBinaryData: testBinaryData,
dataurldata0: dataurldata0,
dataurldata1: dataurldata1,
dataurldata2: dataurldata2 });
worker.onmessage = function(e) {
var msg = e.data;
if (msg.type == 'finish') {
SimpleTest.finish();
return;
}
if (msg.type == 'check') {
ok(msg.status, msg.msg);
return;
}
ok(false, "Unknown message.");
}
}
function convertToUTF16(s) {
res = "";
for (var i = 0; i < s.length; ++i) {
c = s.charCodeAt(i);
res += String.fromCharCode(c & 255, c >>> 8);
}
return res;
}
function convertToUTF8(s) {
return unescape(encodeURIComponent(s));
}
</script>
</body>
</html>

View File

@ -106,6 +106,8 @@ var interfaceNamesInGlobalScope =
"EventTarget",
// IMPORTANT: Do not change this list without review from a DOM peer!
"File",
// IMPORTANT: Do not change this list without review from a DOM peer!
"FileReader",
// IMPORTANT: Do not change this list without review from a DOM peer!
"FileReaderSync",
// IMPORTANT: Do not change this list without review from a DOM peer!

View File

@ -0,0 +1,417 @@
var testRanCounter = 0;
var expectedTestCount = 0;
var testSetupFinished = false;
function ok(a, msg) {
postMessage({type: 'check', status: !!a, msg: msg });
}
function is(a, b, msg) {
ok(a === b, msg);
}
function finish() {
postMessage({type: 'finish'});
}
function convertToUTF16(s) {
res = "";
for (var i = 0; i < s.length; ++i) {
c = s.charCodeAt(i);
res += String.fromCharCode(c & 255, c >>> 8);
}
return res;
}
function convertToUTF8(s) {
return unescape(encodeURIComponent(s));
}
function convertToDataURL(s) {
return "data:application/octet-stream;base64," + btoa(s);
}
onmessage = function(message) {
is(FileReader.EMPTY, 0, "correct EMPTY value");
is(FileReader.LOADING, 1, "correct LOADING value");
is(FileReader.DONE, 2, "correct DONE value");
// List of blobs.
var asciiFile = message.data.blobs.shift();
var binaryFile = message.data.blobs.shift();
var nonExistingFile = message.data.blobs.shift();
var utf8TextFile = message.data.blobs.shift();
var utf16TextFile = message.data.blobs.shift();
var emptyFile = message.data.blobs.shift();
var dataUrlFile0 = message.data.blobs.shift();
var dataUrlFile1 = message.data.blobs.shift();
var dataUrlFile2 = message.data.blobs.shift();
// List of buffers for testing.
var testTextData = message.data.testTextData;
var testASCIIData = message.data.testASCIIData;
var testBinaryData = message.data.testBinaryData;
var dataurldata0 = message.data.dataurldata0;
var dataurldata1 = message.data.dataurldata1;
var dataurldata2 = message.data.dataurldata2;
// Test that plain reading works and fires events as expected, both
// for text and binary reading
var onloadHasRunText = false;
var onloadStartHasRunText = false;
r = new FileReader();
is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading");
r.addEventListener("load", function() { onloadHasRunText = true }, false);
r.addEventListener("loadstart", function() { onloadStartHasRunText = true }, false);
r.readAsText(asciiFile);
is(r.readyState, FileReader.LOADING, "correct loading text readyState");
is(onloadHasRunText, false, "text loading must be async");
is(onloadStartHasRunText, true, "text loadstart should fire sync");
expectedTestCount++;
var onloadHasRunBinary = false;
var onloadStartHasRunBinary = false;
r = new FileReader();
is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
r.addEventListener("load", function() { onloadHasRunBinary = true }, false);
r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true }, false);
r.readAsBinaryString(binaryFile);
r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading");
is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
is(onloadHasRunBinary, false, "binary loading must be async");
is(onloadStartHasRunBinary, true, "binary loadstart should fire sync");
expectedTestCount++;
var onloadHasRunArrayBuffer = false;
var onloadStartHasRunArrayBuffer = false;
r = new FileReader();
is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
r.addEventListener("load", function() { onloadHasRunArrayBuffer = true }, false);
r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true }, false);
r.readAsArrayBuffer(binaryFile);
r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading");
is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async");
is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync");
expectedTestCount++;
// Test a variety of encodings, and make sure they work properly
r = new FileReader();
r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading");
r.readAsText(asciiFile, "");
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading");
r.readAsText(asciiFile, "iso-8859-1");
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandler(testTextData,
convertToUTF8(testTextData).length,
"utf8 reading");
r.readAsText(utf8TextFile, "utf8");
expectedTestCount++;
r = new FileReader();
r.readAsText(utf16TextFile, "utf-16");
r.onload = getLoadHandler(testTextData,
convertToUTF16(testTextData).length,
"utf16 reading");
expectedTestCount++;
// Test get result without reading
r = new FileReader();
is(r.readyState, FileReader.EMPTY,
"readyState in test reader get result without reading");
is(r.error, null,
"no error in test reader get result without reading");
is(r.result, null,
"result in test reader get result without reading");
// Test loading an empty file works (and doesn't crash!)
r = new FileReader();
r.onload = getLoadHandler("", 0, "empty no encoding reading");
r.readAsText(emptyFile, "");
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandler("", 0, "empty utf8 reading");
r.readAsText(emptyFile, "utf8");
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandler("", 0, "empty utf16 reading");
r.readAsText(emptyFile, "utf-16");
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandler("", 0, "empty binary string reading");
r.readAsBinaryString(emptyFile);
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading");
r.readAsArrayBuffer(emptyFile);
expectedTestCount++;
r = new FileReader();
r.onload = getLoadHandler(convertToDataURL(""), 0, "empt binary string reading");
r.readAsDataURL(emptyFile);
expectedTestCount++;
// Test reusing a FileReader to read multiple times
r = new FileReader();
r.onload = getLoadHandler(testASCIIData,
testASCIIData.length,
"to-be-reused reading text")
var makeAnotherReadListener = function(event) {
r = event.target;
r.removeEventListener("load", makeAnotherReadListener, false);
r.onload = getLoadHandler(testASCIIData,
testASCIIData.length,
"reused reading text");
r.readAsText(asciiFile);
};
r.addEventListener("load", makeAnotherReadListener, false);
r.readAsText(asciiFile);
expectedTestCount += 2;
r = new FileReader();
r.onload = getLoadHandler(testBinaryData,
testBinaryData.length,
"to-be-reused reading binary")
var makeAnotherReadListener2 = function(event) {
r = event.target;
r.removeEventListener("load", makeAnotherReadListener2, false);
r.onload = getLoadHandler(testBinaryData,
testBinaryData.length,
"reused reading binary");
r.readAsBinaryString(binaryFile);
};
r.addEventListener("load", makeAnotherReadListener2, false);
r.readAsBinaryString(binaryFile);
expectedTestCount += 2;
r = new FileReader();
r.onload = getLoadHandler(convertToDataURL(testBinaryData),
testBinaryData.length,
"to-be-reused reading data url")
var makeAnotherReadListener3 = function(event) {
r = event.target;
r.removeEventListener("load", makeAnotherReadListener3, false);
r.onload = getLoadHandler(convertToDataURL(testBinaryData),
testBinaryData.length,
"reused reading data url");
r.readAsDataURL(binaryFile);
};
r.addEventListener("load", makeAnotherReadListener3, false);
r.readAsDataURL(binaryFile);
expectedTestCount += 2;
r = new FileReader();
r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
testBinaryData.length,
"to-be-reused reading arrayBuffer")
var makeAnotherReadListener4 = function(event) {
r = event.target;
r.removeEventListener("load", makeAnotherReadListener4, false);
r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
testBinaryData.length,
"reused reading arrayBuffer");
r.readAsArrayBuffer(binaryFile);
};
r.addEventListener("load", makeAnotherReadListener4, false);
r.readAsArrayBuffer(binaryFile);
expectedTestCount += 2;
// Test first reading as ArrayBuffer then read as something else
// (BinaryString) and doesn't crash
r = new FileReader();
r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
testBinaryData.length,
"to-be-reused reading arrayBuffer")
var makeAnotherReadListener5 = function(event) {
r = event.target;
r.removeEventListener("load", makeAnotherReadListener5, false);
r.onload = getLoadHandler(testBinaryData,
testBinaryData.length,
"reused reading binary string");
r.readAsBinaryString(binaryFile);
};
r.addEventListener("load", makeAnotherReadListener5, false);
r.readAsArrayBuffer(binaryFile);
expectedTestCount += 2;
//Test data-URI encoding on differing file sizes
is(dataurldata0.length % 3, 0, "Want to test data with length % 3 == 0");
r = new FileReader();
r.onload = getLoadHandler(convertToDataURL(dataurldata0),
dataurldata0.length,
"dataurl reading, %3 = 0");
r.readAsDataURL(dataUrlFile0);
expectedTestCount++;
is(dataurldata1.length % 3, 1, "Want to test data with length % 3 == 1");
r = new FileReader();
r.onload = getLoadHandler(convertToDataURL(dataurldata1),
dataurldata1.length,
"dataurl reading, %3 = 1");
r.readAsDataURL(dataUrlFile1);
expectedTestCount++;
is(dataurldata2.length % 3, 2, "Want to test data with length % 3 == 2");
r = new FileReader();
r.onload = getLoadHandler(convertToDataURL(dataurldata2),
dataurldata2.length,
"dataurl reading, %3 = 2");
r.readAsDataURL(dataUrlFile2),
expectedTestCount++;
// Test abort()
var abortHasRun = false;
var loadEndHasRun = false;
r = new FileReader();
r.onabort = function (event) {
is(abortHasRun, false, "abort should only fire once");
is(loadEndHasRun, false, "loadend shouldn't have fired yet");
abortHasRun = true;
is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
is(event.target.result, null, "file data should be null on aborted reads");
}
r.onloadend = function (event) {
is(abortHasRun, true, "abort should fire before loadend");
is(loadEndHasRun, false, "loadend should only fire once");
loadEndHasRun = true;
is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
is(event.target.result, null, "file data should be null on aborted reads");
}
r.onload = function() { ok(false, "load should not fire for aborted reads") };
r.onerror = function() { ok(false, "error should not fire for aborted reads") };
r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
var abortThrew = false;
try {
r.abort();
} catch(e) {
abortThrew = true;
}
is(abortThrew, true, "abort() must throw if not loading");
is(abortHasRun, false, "abort() is a no-op unless loading");
r.readAsText(asciiFile);
r.abort();
is(abortHasRun, true, "abort should fire sync");
is(loadEndHasRun, true, "loadend should fire sync");
// Test calling readAsX to cause abort()
var reuseAbortHasRun = false;
r = new FileReader();
r.onabort = function (event) {
is(reuseAbortHasRun, false, "abort should only fire once");
reuseAbortHasRun = true;
is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
is(event.target.result, null, "file data should be null on aborted reads");
}
r.onload = function() { ok(false, "load should not fire for aborted reads") };
var abortThrew = false;
try {
r.abort();
} catch(e) {
abortThrew = true;
}
is(abortThrew, true, "abort() must throw if not loading");
is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
r.readAsText(asciiFile);
r.readAsText(asciiFile);
is(reuseAbortHasRun, true, "abort should fire sync");
r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading");
expectedTestCount++;
// Test reading from nonexistent files
r = new FileReader();
var didThrow = false;
r.onerror = function (event) {
is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror");
is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files");
is(event.target.result, null, "file data should be null on aborted reads");
testHasRun();
};
r.onload = function (event) {
is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
testHasRun();
};
try {
r.readAsDataURL(nonExistingFile);
expectedTestCount++;
} catch(ex) {
didThrow = true;
}
// Once this test passes, we should test that onerror gets called and
// that the FileReader object is in the right state during that call.
is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead");
function getLoadHandler(expectedResult, expectedLength, testName) {
return function (event) {
is(event.target.readyState, FileReader.DONE,
"readyState in test " + testName);
is(event.target.error, null,
"no error in test " + testName);
is(event.target.result, expectedResult,
"result in test " + testName);
is(event.lengthComputable, true,
"lengthComputable in test " + testName);
is(event.loaded, expectedLength,
"loaded in test " + testName);
is(event.total, expectedLength,
"total in test " + testName);
testHasRun();
}
}
function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) {
return function (event) {
is(event.target.readyState, FileReader.DONE,
"readyState in test " + testName);
is(event.target.error, null,
"no error in test " + testName);
is(event.lengthComputable, true,
"lengthComputable in test " + testName);
is(event.loaded, expectedLength,
"loaded in test " + testName);
is(event.total, expectedLength,
"total in test " + testName);
is(event.target.result.byteLength, expectedLength,
"array buffer size in test " + testName);
var u8v = new Uint8Array(event.target.result);
is(String.fromCharCode.apply(String, u8v), expectedResult,
"array buffer contents in test " + testName);
u8v = null;
is(event.target.result.byteLength, expectedLength,
"array buffer size after gc in test " + testName);
u8v = new Uint8Array(event.target.result);
is(String.fromCharCode.apply(String, u8v), expectedResult,
"array buffer contents after gc in test " + testName);
testHasRun();
}
}
function testHasRun() {
//alert(testRanCounter);
++testRanCounter;
if (testRanCounter == expectedTestCount) {
is(testSetupFinished, true, "test setup should have finished; check for exceptions");
is(onloadHasRunText, true, "onload text should have fired by now");
is(onloadHasRunBinary, true, "onload binary should have fired by now");
finish();
}
}
testSetupFinished = true;
}

View File

@ -41,136 +41,3 @@
[FileList interface: attribute length]
expected: FAIL
[FileReader interface: existence and properties of interface object]
expected: FAIL
[FileReader interface object length]
expected: FAIL
[FileReader interface: existence and properties of interface prototype object]
expected: FAIL
[FileReader interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[FileReader interface: operation readAsArrayBuffer(Blob)]
expected: FAIL
[FileReader interface: operation readAsText(Blob,DOMString)]
expected: FAIL
[FileReader interface: operation readAsDataURL(Blob)]
expected: FAIL
[FileReader interface: operation abort()]
expected: FAIL
[FileReader interface: constant EMPTY on interface object]
expected: FAIL
[FileReader interface: constant EMPTY on interface prototype object]
expected: FAIL
[FileReader interface: constant LOADING on interface object]
expected: FAIL
[FileReader interface: constant LOADING on interface prototype object]
expected: FAIL
[FileReader interface: constant DONE on interface object]
expected: FAIL
[FileReader interface: constant DONE on interface prototype object]
expected: FAIL
[FileReader interface: attribute readyState]
expected: FAIL
[FileReader interface: attribute result]
expected: FAIL
[FileReader interface: attribute error]
expected: FAIL
[FileReader interface: attribute onloadstart]
expected: FAIL
[FileReader interface: attribute onprogress]
expected: FAIL
[FileReader interface: attribute onload]
expected: FAIL
[FileReader interface: attribute onabort]
expected: FAIL
[FileReader interface: attribute onerror]
expected: FAIL
[FileReader interface: attribute onloadend]
expected: FAIL
[FileReader must be primary interface of new FileReader()]
expected: FAIL
[Stringification of new FileReader()]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "readAsArrayBuffer" with the proper type (0)]
expected: FAIL
[FileReader interface: calling readAsArrayBuffer(Blob) on new FileReader() with too few arguments must throw TypeError]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "readAsText" with the proper type (1)]
expected: FAIL
[FileReader interface: calling readAsText(Blob,DOMString) on new FileReader() with too few arguments must throw TypeError]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "readAsDataURL" with the proper type (2)]
expected: FAIL
[FileReader interface: calling readAsDataURL(Blob) on new FileReader() with too few arguments must throw TypeError]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "abort" with the proper type (3)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "EMPTY" with the proper type (4)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "LOADING" with the proper type (5)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "DONE" with the proper type (6)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "readyState" with the proper type (7)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "result" with the proper type (8)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "error" with the proper type (9)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "onloadstart" with the proper type (10)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "onprogress" with the proper type (11)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "onload" with the proper type (12)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "onabort" with the proper type (13)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "onerror" with the proper type (14)]
expected: FAIL
[FileReader interface: new FileReader() must inherit property "onloadend" with the proper type (15)]
expected: FAIL

View File

@ -12,7 +12,7 @@
#include "nsIPipe.h"
#include "nsICloneableInputStream.h"
#include "nsIEventTarget.h"
#include "nsIRunnable.h"
#include "nsICancelableRunnable.h"
#include "nsISafeOutputStream.h"
#include "nsString.h"
#include "nsIAsyncInputStream.h"
@ -25,8 +25,11 @@ using namespace mozilla;
//-----------------------------------------------------------------------------
// This is a nsICancelableRunnable because we can dispatch it to Workers and
// those can be shut down at any time, and in these cases, Cancel() is called
// instead of Run().
class nsInputStreamReadyEvent final
: public nsIRunnable
: public nsICancelableRunnable
, public nsIInputStreamCallback
{
public:
@ -95,19 +98,28 @@ public:
return NS_OK;
}
NS_IMETHOD Cancel() override
{
mCallback = nullptr;
return NS_OK;
}
private:
nsCOMPtr<nsIAsyncInputStream> mStream;
nsCOMPtr<nsIInputStreamCallback> mCallback;
nsCOMPtr<nsIEventTarget> mTarget;
};
NS_IMPL_ISUPPORTS(nsInputStreamReadyEvent, nsIRunnable,
nsIInputStreamCallback)
NS_IMPL_ISUPPORTS(nsInputStreamReadyEvent, nsICancelableRunnable,
nsIRunnable, nsIInputStreamCallback)
//-----------------------------------------------------------------------------
// This is a nsICancelableRunnable because we can dispatch it to Workers and
// those can be shut down at any time, and in these cases, Cancel() is called
// instead of Run().
class nsOutputStreamReadyEvent final
: public nsIRunnable
: public nsICancelableRunnable
, public nsIOutputStreamCallback
{
public:
@ -176,14 +188,20 @@ public:
return NS_OK;
}
NS_IMETHOD Cancel() override
{
mCallback = nullptr;
return NS_OK;
}
private:
nsCOMPtr<nsIAsyncOutputStream> mStream;
nsCOMPtr<nsIOutputStreamCallback> mCallback;
nsCOMPtr<nsIEventTarget> mTarget;
};
NS_IMPL_ISUPPORTS(nsOutputStreamReadyEvent, nsIRunnable,
nsIOutputStreamCallback)
NS_IMPL_ISUPPORTS(nsOutputStreamReadyEvent, nsICancelableRunnable,
nsIRunnable, nsIOutputStreamCallback)
//-----------------------------------------------------------------------------
@ -216,7 +234,7 @@ NS_NewOutputStreamReadyEvent(nsIOutputStreamCallback* aCallback,
class nsAStreamCopier
: public nsIInputStreamCallback
, public nsIOutputStreamCallback
, public nsIRunnable
, public nsICancelableRunnable
{
public:
NS_DECL_THREADSAFE_ISUPPORTS
@ -433,6 +451,8 @@ public:
return NS_OK;
}
NS_IMETHOD Cancel() MOZ_MUST_OVERRIDE override = 0;
nsresult PostContinuationEvent()
{
// we cannot post a continuation event if there is currently
@ -489,6 +509,7 @@ protected:
NS_IMPL_ISUPPORTS(nsAStreamCopier,
nsIInputStreamCallback,
nsIOutputStreamCallback,
nsICancelableRunnable,
nsIRunnable)
class nsStreamCopierIB final : public nsAStreamCopier
@ -527,7 +548,8 @@ public:
return state->mSinkCondition;
}
uint32_t DoCopy(nsresult* aSourceCondition, nsresult* aSinkCondition)
uint32_t DoCopy(nsresult* aSourceCondition,
nsresult* aSinkCondition) override
{
ReadSegmentsState state;
state.mSink = mSink;
@ -539,6 +561,11 @@ public:
*aSinkCondition = state.mSinkCondition;
return n;
}
NS_IMETHOD Cancel() override
{
return NS_OK;
}
};
class nsStreamCopierOB final : public nsAStreamCopier
@ -577,7 +604,8 @@ public:
return state->mSourceCondition;
}
uint32_t DoCopy(nsresult* aSourceCondition, nsresult* aSinkCondition)
uint32_t DoCopy(nsresult* aSourceCondition,
nsresult* aSinkCondition) override
{
WriteSegmentsState state;
state.mSource = mSource;
@ -589,6 +617,11 @@ public:
*aSourceCondition = state.mSourceCondition;
return n;
}
NS_IMETHOD Cancel() override
{
return NS_OK;
}
};
//-----------------------------------------------------------------------------

View File

@ -127,10 +127,23 @@ public:
} // namespace
class nsTimerEvent : public nsRunnable
// This is a nsICancelableRunnable because we can dispatch it to Workers and
// those can be shut down at any time, and in these cases, Cancel() is called
// instead of Run().
class nsTimerEvent : public nsCancelableRunnable
{
public:
NS_IMETHOD Run();
NS_IMETHOD Run() override;
NS_IMETHOD Cancel() override
{
// Since nsTimerImpl is not thread-safe, we should release |mTimer|
// here in the target thread to avoid race condition. Otherwise,
// ~nsTimerEvent() which calls nsTimerImpl::Release() could run in the
// timer thread and result in race condition.
mTimer = nullptr;
return NS_OK;
}
nsTimerEvent()
: mTimer()
@ -253,6 +266,8 @@ nsTimerEvent::DeleteAllocatorIfNeeded()
NS_IMETHODIMP
nsTimerEvent::Run()
{
MOZ_ASSERT(mTimer);
if (mGeneration != mTimer->GetGeneration()) {
return NS_OK;
}
@ -265,13 +280,10 @@ nsTimerEvent::Run()
}
mTimer->Fire();
// Since nsTimerImpl is not thread-safe, we should release |mTimer|
// here in the target thread to avoid race condition. Otherwise,
// ~nsTimerEvent() which calls nsTimerImpl::Release() could run in the
// timer thread and result in race condition.
mTimer = nullptr;
return NS_OK;
// We call Cancel() to correctly release mTimer.
// Read more in the Cancel() implementation.
return Cancel();
}
nsresult

View File

@ -16,6 +16,7 @@ interface nsICancelableRunnable : nsIRunnable
/**
* Cancels a pending task. If the task has already been executed this will
* be a no-op. Calling this method twice is considered an error.
* If cancel() is called, run() will not be called.
*
* @throws NS_ERROR_UNEXPECTED
* Indicates that the runnable has already been canceled.