Backed out changeset 631e22733b8b (bug 1724687) for causing multiple failures. CLOSED TREE

This commit is contained in:
Sandor Molnar 2022-12-23 02:48:55 +02:00
Родитель 8e681217e2
Коммит cfe9cd7773
8 изменённых файлов: 214 добавлений и 218 удалений

Просмотреть файл

@ -2,59 +2,18 @@
* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
UnsupportedError: "chrome://remote/content/cdp/Error.sys.mjs",
});
export class Stream {
#path;
#offset;
#length;
constructor(path) {
this.#path = path;
this.#offset = 0;
this.#length = null;
}
async destroy() {
await IOUtils.remove(this.#path);
}
async seek(seekTo) {
// To keep compatibility with Chrome clip invalid offsets
this.#offset = Math.max(0, Math.min(seekTo, await this.length()));
}
async readBytes(count) {
const bytes = await IOUtils.read(this.#path, {
offset: this.#offset,
maxBytes: count,
});
this.#offset += bytes.length;
return bytes;
}
async available() {
const length = await this.length();
return length - this.#offset;
}
async length() {
if (this.#length === null) {
const info = await IOUtils.stat(this.#path);
this.#length = info.size;
}
return this.#length;
}
get path() {
return this.#path;
}
}
XPCOMUtils.defineLazyModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
OS: "resource://gre/modules/osfile.jsm",
});
export class StreamRegistry {
constructor() {
@ -63,8 +22,8 @@ export class StreamRegistry {
// Register an async shutdown blocker to ensure all open IO streams are
// closed, and remaining temporary files removed. Needs to happen before
// IOUtils has been shutdown.
IOUtils.profileBeforeChange.addBlocker(
// OS.File has been shutdown.
lazy.AsyncShutdown.profileBeforeChange.addBlocker(
"Remote Agent: Clean-up of open streams",
async () => {
await this.destructor();
@ -74,33 +33,52 @@ export class StreamRegistry {
async destructor() {
for (const stream of this.streams.values()) {
await stream.destroy();
await this._discard(stream);
}
this.streams.clear();
}
async _discard(stream) {
if (stream instanceof lazy.OS.File) {
let fileInfo;
// Also remove the temporary file
try {
fileInfo = await stream.stat();
stream.close();
await lazy.OS.File.remove(fileInfo.path, { ignoreAbsent: true });
} catch (e) {
console.error(`Failed to remove ${fileInfo?.path}: ${e.message}`);
}
}
}
/**
* Add a new stream to the registry.
*
* @param {string} path
* The path to the file to use as a stream.
* @param {OS.File} stream
* Instance of the stream to add.
*
* @return {string}
* Stream handle (uuid)
*/
add(stream) {
if (!(stream instanceof Stream)) {
let handle;
if (stream instanceof lazy.OS.File) {
handle = Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
} else {
// Bug 1602731 - Implement support for blob
throw new lazy.UnsupportedError(`Unknown stream type for ${stream}`);
}
const handle = Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
this.streams.set(handle, stream);
return handle;
}
@ -110,8 +88,8 @@ export class StreamRegistry {
* @param {string} handle
* Handle of the stream to retrieve.
*
* @return {Stream}
* The requested stream.
* @return {OS.File}
* Requested stream
*/
get(handle) {
const stream = this.streams.get(handle);
@ -134,7 +112,7 @@ export class StreamRegistry {
*/
async remove(handle) {
const stream = this.get(handle);
await stream.destroy();
await this._discard(stream);
return this.streams.delete(handle);
}

Просмотреть файл

@ -2,9 +2,17 @@
* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
import { StreamRegistry } from "chrome://remote/content/cdp/StreamRegistry.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyModuleGetters(lazy, {
OS: "resource://gre/modules/osfile.jsm",
});
const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
// Global singleton for managing open streams
@ -55,16 +63,20 @@ export class IO extends Domain {
}
const stream = streamRegistry.get(handle);
const fileInfo = await stream.stat();
if (typeof offset != "undefined") {
if (typeof offset != "number") {
throw new TypeError(`offset: integer value expected`);
}
await stream.seek(offset);
// To keep compatibility with Chrome clip invalid offsets
const seekTo = Math.max(0, Math.min(offset, fileInfo.size));
await stream.setPosition(seekTo, lazy.OS.File.POS_START);
}
const remainingBytes = await stream.available();
const curPos = await stream.getPosition();
const remainingBytes = fileInfo.size - curPos;
let chunkSize;
if (typeof size != "undefined") {
@ -79,8 +91,7 @@ export class IO extends Domain {
chunkSize = Math.min(DEFAULT_CHUNK_SIZE, remainingBytes);
}
const bytes = await stream.readBytes(chunkSize);
const bytes = await stream.read(chunkSize);
// Each UCS2 character has an upper byte of 0 and a lower byte matching
// the binary data. Using a loop here prevents us from hitting the browser's
// internal `arguments.length` limit.

Просмотреть файл

@ -2,6 +2,8 @@
* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
const lazy = {};
@ -15,12 +17,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
"chrome://remote/content/cdp/domains/parent/page/DialogHandler.sys.mjs",
PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
streamRegistry: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs",
Stream: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
UnsupportedError: "chrome://remote/content/cdp/Error.sys.mjs",
windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
OS: "resource://gre/modules/osfile.jsm",
});
const MAX_CANVAS_DIMENSION = 32767;
const MAX_CANVAS_AREA = 472907776;
@ -540,9 +545,9 @@ export class Page extends Domain {
* Return as base64-encoded string (ReturnAsBase64),
* or stream (ReturnAsStream). Defaults to ReturnAsBase64.
*
* @return {Promise<{data:string, stream:Stream}>}
* @return {Promise<{data:string, stream:string}>
* Based on the transferMode setting data is a base64-encoded string,
* or stream is a Stream.
* or stream is a handle to a OS.File stream.
*/
async printToPDF(options = {}) {
const {
@ -588,8 +593,13 @@ export class Page extends Domain {
throw new TypeError("paperWidth is zero or negative");
}
let path;
let stream;
// Create a unique filename for the temporary PDF file
const basePath = lazy.OS.Path.join(
lazy.OS.Constants.Path.tmpDir,
"remote-agent.pdf"
);
const { file, path: filePath } = await lazy.OS.File.openUnique(basePath);
await file.close();
const psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
Ci.nsIPrintSettingsService
@ -601,37 +611,9 @@ export class Page extends Domain {
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
printSettings.printerName = "";
printSettings.printSilent = true;
if (transferMode === PDF_TRANSFER_MODES.stream) {
// If we are returning a stream, we write the PDF to disk so that we don't
// keep (potentially very large) PDFs in memory. We can then stream them
// to the client via the returned Stream.
//
// NOTE: This is a potentially premature optimization -- it might be fine
// to keep these PDFs in memory, but we don't have specifics on how CDP is
// used in the field so it is possible that leaving the PDFs in memory
// could cause a regression.
path = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"remote-agent.pdf"
);
printSettings.outputDestination =
Ci.nsIPrintSettings.kOutputDestinationFile;
printSettings.toFileName = path;
} else {
// If we are returning the data immediately, there is no sense writing it
// to disk only to read it later.
const UINT32_MAX = 0xffffffff;
stream = Cc["@mozilla.org/storagestream;1"].createInstance(
Ci.nsIStorageStream
);
stream.init(4096, UINT32_MAX);
printSettings.outputDestination =
Ci.nsIPrintSettings.kOutputDestinationStream;
printSettings.outputStream = stream.getOutputStream(0);
}
printSettings.outputDestination =
Ci.nsIPrintSettings.kOutputDestinationFile;
printSettings.toFileName = filePath;
printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches;
printSettings.paperWidth = paperWidth;
@ -663,7 +645,6 @@ export class Page extends Domain {
const { linkedBrowser } = this.session.target.tab;
await linkedBrowser.browsingContext.print(printSettings);
// TODO: Bug 1785046 fixes this.
// Bug 1603739 - With e10s enabled the promise returned by print() resolves
// too early, which means the file hasn't been completely written.
@ -672,36 +653,33 @@ export class Page extends Domain {
let lastSize = 0;
const timerId = lazy.setInterval(async () => {
if (transferMode === PDF_TRANSFER_MODES.stream) {
const fileInfo = await IOUtils.stat(path);
if (lastSize > 0 && fileInfo.size == lastSize) {
lazy.clearInterval(timerId);
resolve();
}
lastSize = fileInfo.size;
} else if (!stream.writeInProgress) {
const fileInfo = await lazy.OS.File.stat(filePath);
if (lastSize > 0 && fileInfo.size == lastSize) {
lazy.clearInterval(timerId);
resolve();
}
lastSize = fileInfo.size;
}, DELAY_CHECK_FILE_COMPLETELY_WRITTEN);
});
const fp = await lazy.OS.File.open(filePath);
const retval = { data: null, stream: null };
if (transferMode == PDF_TRANSFER_MODES.stream) {
retval.stream = lazy.streamRegistry.add(new lazy.Stream(path));
retval.stream = lazy.streamRegistry.add(fp);
} else {
const inputStream = Cc["@mozilla.org/binaryinputstream"].createInstance(
Ci.nsIBinaryInputStream
);
inputStream.setInputStream(stream.getInputStream(0));
// return all data as a base64 encoded string
let bytes;
try {
bytes = await fp.read();
} finally {
fp.close();
await lazy.OS.File.remove(filePath);
}
const available = inputStream.available();
const bytes = inputStream.readBytes(available);
retval.data = btoa(bytes);
stream.close();
// Each UCS2 character has an upper byte of 0 and a lower byte matching
// the binary data
retval.data = btoa(String.fromCharCode.apply(null, bytes));
}
return retval;

Просмотреть файл

@ -3,6 +3,8 @@
"use strict";
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { RemoteAgent } = ChromeUtils.importESModule(
"chrome://remote/content/components/RemoteAgent.sys.mjs"
);
@ -12,9 +14,6 @@ const { RemoteAgentError } = ChromeUtils.importESModule(
const { TabManager } = ChromeUtils.importESModule(
"chrome://remote/content/shared/TabManager.sys.mjs"
);
const { Stream } = ChromeUtils.importESModule(
"chrome://remote/content/cdp/StreamRegistry.sys.mjs"
);
const TIMEOUT_MULTIPLIER = SpecialPowers.isDebugBuild ? 4 : 1;
const TIMEOUT_EVENTS = 1000 * TIMEOUT_MULTIPLIER;
@ -388,7 +387,7 @@ function fail(message) {
}
/**
* Create a stream with the specified contents.
* Create a file with the specified contents.
*
* @param {string} contents
* Contents of the file.
@ -398,26 +397,42 @@ function fail(message) {
* @param {boolean=} options.remove
* If true, automatically remove the file after the test. Defaults to true.
*
* @return {Promise<Stream>}
* @return {Promise}
* @resolves {string}
* Returns the final path of the created file.
*/
async function createFileStream(contents, options = {}) {
async function createFile(contents, options = {}) {
let { path = null, remove = true } = options;
if (!path) {
path = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"remote-agent.txt"
);
const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.txt");
const { file, path: tmpPath } = await OS.File.openUnique(basePath, {
humanReadable: true,
});
await file.close();
path = tmpPath;
}
await IOUtils.writeUTF8(path, contents);
let encoder = new TextEncoder();
let array = encoder.encode(contents);
const stream = new Stream(path);
const count = await OS.File.writeAtomic(path, array, {
encoding: "utf-8",
tmpPath: path + ".tmp",
});
is(count, contents.length, "All data has been written to file");
const file = await OS.File.open(path);
// Automatically remove the file once the test has finished
if (remove) {
registerCleanupFunction(() => stream.destroy());
registerCleanupFunction(async () => {
await file.close();
await OS.File.remove(path, { ignoreAbsent: true });
});
}
return stream;
return { file, path };
}
async function throwScriptError(options = {}) {

Просмотреть файл

@ -6,13 +6,10 @@
add_task(async function fileRemovedAfterClose({ client }) {
const { IO } = client;
const contents = "Lorem ipsum";
const { handle } = await registerFileStream(contents);
const { handle, path } = await registerFileStream(contents);
await IO.close({ handle });
ok(
!(await IOUtils.exists(handle.path)),
"Discarded the temporary backing storage"
);
ok(!(await OS.File.exists(path)), "Discarded the temporary backing storage");
});
add_task(async function unknownHandle({ client }) {

Просмотреть файл

@ -73,21 +73,10 @@ add_task(async function readBySize({ client }) {
add_task(async function readAfterClose({ client }) {
const { IO } = client;
const contents = "Lorem ipsum";
// If we omit remove: false, then by the time the registered cleanup function
// runs we will have deleted our temp file (in the following call to IO.close)
// *but* another test will have created a file with the same name (due to the
// way IOUtils.createUniqueFile works). That file's stream will not be closed
// and so we won't be able to delete it, resulting in an exception and
// therefore a test failure.
const { handle, stream } = await registerFileStream(contents, {
remove: false,
});
const { handle } = await registerFileStream(contents);
await IO.close({ handle });
ok(!(await IOUtils.exists(stream.path)), "File should no longer exist");
try {
await IO.read({ handle });
ok(false, "Read shouldn't pass");

Просмотреть файл

@ -14,9 +14,13 @@ const { streamRegistry } = ChromeUtils.importESModule(
"chrome://remote/content/cdp/domains/parent/IO.sys.mjs"
);
async function registerFileStream(contents, options) {
const stream = await createFileStream(contents, options);
const handle = streamRegistry.add(stream);
async function registerFileStream(contents, options = {}) {
// Any file as registered with the stream registry will be automatically
// deleted during the shutdown of Firefox.
options.remove = false;
return { handle, stream };
const { file, path } = await createFile(contents, options);
const handle = streamRegistry.add(file);
return { handle, path };
}

Просмотреть файл

@ -3,142 +3,166 @@
"use strict";
const { Stream, StreamRegistry } = ChromeUtils.importESModule(
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { StreamRegistry } = ChromeUtils.importESModule(
"chrome://remote/content/cdp/StreamRegistry.sys.mjs"
);
add_task(function test_constructor() {
add_test(function test_constructor() {
const registry = new StreamRegistry();
equal(registry.streams.size, 0);
run_next_test();
});
add_task(async function test_destructor() {
add_test(async function test_destructor() {
const registry = new StreamRegistry();
const { file: file1, path: path1 } = await createFile("foo bar");
const { file: file2, path: path2 } = await createFile("foo bar");
const stream1 = await createFileStream("foo bar");
const stream2 = await createFileStream("foo bar");
const handle1 = registry.add(stream1);
const handle2 = registry.add(stream2);
registry.add(file1);
registry.add(file2);
equal(registry.streams.size, 2);
await registry.destructor();
equal(registry.streams.size, 0);
ok(!(await OS.File.exists(path1)), "temporary file has been removed");
ok(!(await OS.File.exists(path2)), "temporary file has been removed");
ok(!(await IOUtils.exists(handle1.path)), "temporary file has been removed");
ok(!(await IOUtils.exists(handle2.path)), "temporary file has been removed");
run_next_test();
});
add_task(async function test_addValidStreamType() {
add_test(async function test_addValidStreamType() {
const registry = new StreamRegistry();
const { file } = await createFile("foo bar");
const stream = await createFileStream("foo bar");
const handle = registry.add(stream);
const handle = registry.add(file);
equal(registry.streams.size, 1, "A single stream has been added");
equal(typeof handle, "string", "Handle is of type string");
ok(registry.streams.has(handle), "Handle has been found");
equal(registry.streams.get(handle), file, "Expected OS.File stream found");
const rv = registry.streams.get(handle);
equal(rv, stream, "Expected stream found");
run_next_test();
});
add_task(async function test_addCreatesDifferentHandles() {
add_test(async function test_addCreatesDifferentHandles() {
const registry = new StreamRegistry();
const stream = await createFileStream("foo bar");
const { file } = await createFile("foo bar");
const handle1 = registry.add(stream);
const handle1 = registry.add(file);
equal(registry.streams.size, 1, "A single stream has been added");
equal(typeof handle1, "string", "Handle is of type string");
ok(registry.streams.has(handle1), "Handle has been found");
equal(registry.streams.get(handle1), stream, "Expected stream found");
equal(registry.streams.get(handle1), file, "Expected OS.File stream found");
const handle2 = registry.add(stream);
const handle2 = registry.add(file);
equal(registry.streams.size, 2, "A single stream has been added");
equal(typeof handle2, "string", "Handle is of type string");
ok(registry.streams.has(handle2), "Handle has been found");
equal(registry.streams.get(handle2), stream, "Expected stream found");
equal(registry.streams.get(handle2), file, "Expected OS.File stream found");
notEqual(handle1, handle2, "Different handles have been generated");
run_next_test();
});
add_task(async function test_addInvalidStreamType() {
add_test(async function test_addInvalidStreamType() {
const registry = new StreamRegistry();
Assert.throws(() => registry.add(new Blob([])), /UnsupportedError/);
run_next_test();
});
add_task(async function test_getForValidHandle() {
add_test(async function test_getForValidHandle() {
const registry = new StreamRegistry();
const stream = await createFileStream("foo bar");
const handle = registry.add(stream);
const { file } = await createFile("foo bar");
const handle = registry.add(file);
equal(registry.streams.size, 1, "A single stream has been added");
equal(registry.get(handle), stream, "Expected stream found");
equal(registry.get(handle), file, "Expected OS.File stream found");
run_next_test();
});
add_task(async function test_getForInvalidHandle() {
add_test(async function test_getForInvalidHandle() {
const registry = new StreamRegistry();
const stream = await createFileStream("foo bar");
registry.add(stream);
const { file } = await createFile("foo bar");
registry.add(file);
equal(registry.streams.size, 1, "A single stream has been added");
Assert.throws(() => registry.get("foo"), /TypeError/);
run_next_test();
});
add_task(async function test_removeForValidHandle() {
add_test(async function test_removeForValidHandle() {
const registry = new StreamRegistry();
const stream1 = await createFileStream("foo bar");
const stream2 = await createFileStream("foo bar");
const { file: file1, path: path1 } = await createFile("foo bar");
const { file: file2, path: path2 } = await createFile("foo bar");
const handle1 = registry.add(stream1);
const handle2 = registry.add(stream2);
const handle1 = registry.add(file1);
const handle2 = registry.add(file2);
equal(registry.streams.size, 2);
await registry.remove(handle1);
equal(registry.streams.size, 1);
equal(registry.get(handle2), stream2, "Second stream has not been closed");
ok(
!(await OS.File.exists(path1)),
"temporary file for first stream has been removed"
);
equal(registry.get(handle2), file2, "Second stream has not been closed");
ok(
await OS.File.exists(path2),
"temporary file for second stream hasn't been removed"
);
run_next_test();
});
add_task(async function test_removeForInvalidHandle() {
add_test(async function test_removeForInvalidHandle() {
const registry = new StreamRegistry();
const stream = await createFileStream("foo bar");
registry.add(stream);
const { file } = await createFile("foo bar");
registry.add(file);
equal(registry.streams.size, 1, "A single stream has been added");
await Assert.rejects(registry.remove("foo"), /TypeError/);
run_next_test();
});
/**
* Create a stream with the specified contents.
*
* @param {string} contents
* Contents of the file.
* @param {Object} options
* @param {string=} options.path
* Path of the file. Defaults to the temporary directory.
* @param {boolean=} options.remove
* If true, automatically remove the file after the test. Defaults to true.
*
* @return {Promise<Stream>}
*/
async function createFileStream(contents, options = {}) {
async function createFile(contents, options = {}) {
let { path = null, remove = true } = options;
if (!path) {
path = await IOUtils.createUniqueFile(
PathUtils.tempDir,
"remote-agent.txt"
);
const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.txt");
const { file, path: tmpPath } = await OS.File.openUnique(basePath, {
humanReadable: true,
});
await file.close();
path = tmpPath;
}
await IOUtils.writeUTF8(path, contents);
let encoder = new TextEncoder();
let array = encoder.encode(contents);
const stream = new Stream(path);
const count = await OS.File.writeAtomic(path, array, {
encoding: "utf-8",
tmpPath: path + ".tmp",
});
equal(count, contents.length, "All data has been written to file");
const file = await OS.File.open(path);
// Automatically remove the file once the test has finished
if (remove) {
registerCleanupFunction(() => stream.destroy());
registerCleanupFunction(async () => {
await file.close();
await OS.File.remove(path, { ignoreAbsent: true });
});
}
return stream;
return { file, path };
}