445 lines
15 KiB
JavaScript
445 lines
15 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
var { settleAll } = require("resource://devtools/shared/DevToolsUtils.js");
|
|
var EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
|
|
|
var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
|
|
var {
|
|
getStack,
|
|
callFunctionWithAsyncStack,
|
|
} = require("resource://devtools/shared/platform/stack.js");
|
|
|
|
/**
|
|
* Base class for client-side actor fronts.
|
|
*
|
|
* @param [DevToolsClient|null] conn
|
|
* The conn must either be DevToolsClient or null. Must have
|
|
* addActorPool, removeActorPool, and poolFor.
|
|
* conn can be null if the subclass provides a conn property.
|
|
* @param [Target|null] target
|
|
* If we are instantiating a target-scoped front, this is a reference to the front's
|
|
* Target instance, otherwise this is null.
|
|
* @param [Front|null] parentFront
|
|
* The parent front. This is only available if the Front being initialized is a child
|
|
* of a parent front.
|
|
* @constructor
|
|
*/
|
|
class Front extends Pool {
|
|
constructor(conn = null, targetFront = null, parentFront = null) {
|
|
super(conn);
|
|
if (!conn) {
|
|
throw new Error("Front without conn");
|
|
}
|
|
this.actorID = null;
|
|
// The targetFront attribute represents the debuggable context. Only target-scoped
|
|
// fronts and their children fronts will have the targetFront attribute set.
|
|
this.targetFront = targetFront;
|
|
// The parentFront attribute points to its parent front. Only children of
|
|
// target-scoped fronts will have the parentFront attribute set.
|
|
this.parentFront = parentFront;
|
|
this._requests = [];
|
|
|
|
// Front listener functions registered via `watchFronts`
|
|
this._frontCreationListeners = null;
|
|
this._frontDestructionListeners = null;
|
|
|
|
// List of optional listener for each event, that is processed immediatly on packet
|
|
// receival, before emitting event via EventEmitter on the Front.
|
|
// These listeners are register via Front.before function.
|
|
// Map(Event Name[string] => Event Listener[function])
|
|
this._beforeListeners = new Map();
|
|
|
|
// This flag allows to check if the `initialize` method has resolved.
|
|
// Used to avoid notifying about initialized fronts in `watchFronts`.
|
|
this._initializeResolved = false;
|
|
}
|
|
|
|
/**
|
|
* Return the parent front.
|
|
*/
|
|
getParent() {
|
|
return this.parentFront && this.parentFront.actorID
|
|
? this.parentFront
|
|
: null;
|
|
}
|
|
|
|
destroy() {
|
|
// Prevent destroying twice if a `forwardCancelling` event has already been received
|
|
// and already called `baseFrontClassDestroy`
|
|
this.baseFrontClassDestroy();
|
|
|
|
// Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be
|
|
// able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests.
|
|
this.clearEvents();
|
|
}
|
|
|
|
// This method is also called from `DevToolsClient`, when a connector is destroyed
|
|
// and we should:
|
|
// - reject all pending request made to the remote process/target/thread.
|
|
// - avoid trying to do new request against this remote context.
|
|
// - unmanage this front, so that DevToolsClient.getFront no longer returns it.
|
|
//
|
|
// When a connector is destroyed a `forwardCancelling` RDP event is sent by the server.
|
|
// This is done in a distinct method from `destroy` in order to do all that immediately,
|
|
// even if `Front.destroy` is overloaded by an async method.
|
|
baseFrontClassDestroy() {
|
|
// Reject all outstanding requests, they won't make sense after
|
|
// the front is destroyed.
|
|
while (this._requests.length) {
|
|
const { deferred, to, type, stack } = this._requests.shift();
|
|
// Note: many tests are ignoring `Connection closed` promise rejections,
|
|
// via PromiseTestUtils.allowMatchingRejectionsGlobally.
|
|
// Do not update the message without updating the tests.
|
|
const msg =
|
|
"Connection closed, pending request to " +
|
|
to +
|
|
", type " +
|
|
type +
|
|
" failed" +
|
|
"\n\nRequest stack:\n" +
|
|
stack.formattedStack;
|
|
deferred.reject(new Error(msg));
|
|
}
|
|
|
|
if (this.actorID) {
|
|
super.destroy();
|
|
this.actorID = null;
|
|
}
|
|
this._isDestroyed = true;
|
|
|
|
this.targetFront = null;
|
|
this.parentFront = null;
|
|
this._frontCreationListeners = null;
|
|
this._frontDestructionListeners = null;
|
|
this._beforeListeners = null;
|
|
}
|
|
|
|
async manage(front, form, ctx) {
|
|
if (!front.actorID) {
|
|
throw new Error(
|
|
"Can't manage front without an actor ID.\n" +
|
|
"Ensure server supports " +
|
|
front.typeName +
|
|
"."
|
|
);
|
|
}
|
|
|
|
if (front.parentFront && front.parentFront !== this) {
|
|
throw new Error(
|
|
`${this.actorID} (${this.typeName}) can't manage ${front.actorID}
|
|
(${front.typeName}) since it has a different parentFront ${
|
|
front.parentFront
|
|
? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
|
|
: "<no parentFront>"
|
|
}`
|
|
);
|
|
}
|
|
|
|
super.manage(front);
|
|
|
|
if (typeof front.initialize == "function") {
|
|
await front.initialize();
|
|
}
|
|
front._initializeResolved = true;
|
|
|
|
// Ensure calling form() *before* notifying about this front being just created.
|
|
// We exprect the front to be fully initialized, especially via its form attributes.
|
|
// But do that *after* calling manage() so that the front is already registered
|
|
// in Pools and can be fetched by its ID, in case a child actor, created in form()
|
|
// tries to get a reference to its parent via the actor ID.
|
|
if (form) {
|
|
front.form(form, ctx);
|
|
}
|
|
|
|
// Call listeners registered via `watchFronts` method
|
|
// (ignore if this front has been destroyed)
|
|
if (this._frontCreationListeners) {
|
|
this._frontCreationListeners.emit(front.typeName, front);
|
|
}
|
|
}
|
|
|
|
async unmanage(front) {
|
|
super.unmanage(front);
|
|
|
|
// Call listeners registered via `watchFronts` method
|
|
if (this._frontDestructionListeners) {
|
|
this._frontDestructionListeners.emit(front.typeName, front);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Listen for the creation and/or destruction of fronts matching one of the provided types.
|
|
*
|
|
* @param {String} typeName
|
|
* Actor type to watch.
|
|
* @param {Function} onAvailable (optional)
|
|
* Callback fired when a front has been just created or was already available.
|
|
* The function is called with one arguments, the front.
|
|
* @param {Function} onDestroy (optional)
|
|
* Callback fired in case of front destruction.
|
|
* The function is called with the same argument than onAvailable.
|
|
*/
|
|
watchFronts(typeName, onAvailable, onDestroy) {
|
|
if (this.isDestroyed()) {
|
|
// The front was already destroyed, bail out.
|
|
console.error(
|
|
`Tried to call watchFronts for the '${typeName}' type on an ` +
|
|
`already destroyed front '${this.typeName}'.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (onAvailable) {
|
|
// First fire the callback on fronts with the correct type and which have
|
|
// been initialized. If initialize() is still in progress, the front will
|
|
// be emitted via _frontCreationListeners shortly after.
|
|
for (const front of this.poolChildren()) {
|
|
if (front.typeName == typeName && front._initializeResolved) {
|
|
onAvailable(front);
|
|
}
|
|
}
|
|
|
|
if (!this._frontCreationListeners) {
|
|
this._frontCreationListeners = new EventEmitter();
|
|
}
|
|
// Then register the callback for fronts instantiated in the future
|
|
this._frontCreationListeners.on(typeName, onAvailable);
|
|
}
|
|
|
|
if (onDestroy) {
|
|
if (!this._frontDestructionListeners) {
|
|
this._frontDestructionListeners = new EventEmitter();
|
|
}
|
|
this._frontDestructionListeners.on(typeName, onDestroy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop listening for the creation and/or destruction of a given type of fronts.
|
|
* See `watchFronts()` for documentation of the arguments.
|
|
*/
|
|
unwatchFronts(typeName, onAvailable, onDestroy) {
|
|
if (this.isDestroyed()) {
|
|
// The front was already destroyed, bail out.
|
|
console.error(
|
|
`Tried to call unwatchFronts for the '${typeName}' type on an ` +
|
|
`already destroyed front '${this.typeName}'.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (onAvailable && this._frontCreationListeners) {
|
|
this._frontCreationListeners.off(typeName, onAvailable);
|
|
}
|
|
if (onDestroy && this._frontDestructionListeners) {
|
|
this._frontDestructionListeners.off(typeName, onDestroy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register an event listener that will be called immediately on packer receival.
|
|
* The given callback is going to be called before emitting the event via EventEmitter
|
|
* API on the Front. Event emitting will be delayed if the callback is async.
|
|
* Only one such listener can be registered per type of event.
|
|
*
|
|
* @param String type
|
|
* Event emitted by the actor to intercept.
|
|
* @param Function callback
|
|
* Function that will process the event.
|
|
*/
|
|
before(type, callback) {
|
|
if (this._beforeListeners.has(type)) {
|
|
throw new Error(
|
|
`Can't register multiple before listeners for "${type}".`
|
|
);
|
|
}
|
|
this._beforeListeners.set(type, callback);
|
|
}
|
|
|
|
toString() {
|
|
return "[Front for " + this.typeName + "/" + this.actorID + "]";
|
|
}
|
|
|
|
/**
|
|
* Update the actor from its representation.
|
|
* Subclasses should override this.
|
|
*/
|
|
form() {}
|
|
|
|
/**
|
|
* Send a packet on the connection.
|
|
*
|
|
* @param {Object} packet
|
|
* @param {Object} options
|
|
* @param {Boolean} options.bulk
|
|
* To be set to true, if the packet relates to bulk request.
|
|
* Bulk request allows to send raw bytes over the wire instead of
|
|
* having to create a JSON string packet.
|
|
* @param {Function} options.clientBulkCallback
|
|
* For bulk request, function called with a StreamWriter as only argument.
|
|
* This is called when the StreamWriter is available in order to send
|
|
* bytes to the server.
|
|
*/
|
|
send(packet, { bulk = false, clientBulkCallback = null } = {}) {
|
|
// The connection might be closed during the promise resolution
|
|
if (!this.conn?._transport) {
|
|
return;
|
|
}
|
|
|
|
if (!bulk) {
|
|
if (!packet.to) {
|
|
packet.to = this.actorID;
|
|
}
|
|
this.conn._transport.send(packet);
|
|
} else {
|
|
if (!packet.actor) {
|
|
packet.actor = this.actorID;
|
|
}
|
|
this.conn._transport.startBulkSend(packet).then(clientBulkCallback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a two-way request on the connection.
|
|
*
|
|
* See `send()` jsdoc for parameters definition.
|
|
*/
|
|
request(packet, { bulk = false, clientBulkCallback = null } = {}) {
|
|
const deferred = Promise.withResolvers();
|
|
// Save packet basics for debugging
|
|
const { to, type } = packet;
|
|
this._requests.push({
|
|
deferred,
|
|
to: to || this.actorID,
|
|
type,
|
|
packet,
|
|
stack: getStack(),
|
|
clientBulkCallback,
|
|
});
|
|
this.send(packet, { bulk, clientBulkCallback });
|
|
return deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* Handler for incoming packets from the client's actor.
|
|
*/
|
|
onPacket(packet) {
|
|
if (this.isDestroyed()) {
|
|
// If the Front was already destroyed, all the requests have been purged
|
|
// and rejected with detailed error messages in baseFrontClassDestroy.
|
|
return;
|
|
}
|
|
|
|
// Pick off event packets
|
|
const type = packet.type || undefined;
|
|
if (this._clientSpec.events && this._clientSpec.events.has(type)) {
|
|
const event = this._clientSpec.events.get(packet.type);
|
|
let args;
|
|
try {
|
|
args = event.request.read(packet, this);
|
|
} catch (ex) {
|
|
console.error("Error reading event: " + packet.type);
|
|
console.exception(ex);
|
|
throw ex;
|
|
}
|
|
// Check for "pre event" callback to be processed before emitting events on fronts
|
|
// Use event.name instead of packet.type to use specific event name instead of RDP
|
|
// packet's type.
|
|
const beforeEvent = this._beforeListeners.get(event.name);
|
|
if (beforeEvent) {
|
|
const result = beforeEvent.apply(this, args);
|
|
// Check to see if the beforeEvent returned a promise -- if so,
|
|
// wait for their resolution before emitting. Otherwise, emit synchronously.
|
|
if (result && typeof result.then == "function") {
|
|
result.then(() => {
|
|
super.emit(event.name, ...args);
|
|
ChromeUtils.addProfilerMarker(
|
|
"DevTools:RDP Front",
|
|
null,
|
|
`${this.typeName}.${event.name}`
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
super.emit(event.name, ...args);
|
|
ChromeUtils.addProfilerMarker(
|
|
"DevTools:RDP Front",
|
|
null,
|
|
`${this.typeName}.${event.name}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Remaining packets must be responses.
|
|
if (this._requests.length === 0) {
|
|
const msg =
|
|
"Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
|
|
const err = Error(msg);
|
|
console.error(err);
|
|
throw err;
|
|
}
|
|
|
|
const { deferred, packet: clientPacket, stack } = this._requests.shift();
|
|
callFunctionWithAsyncStack(
|
|
() => {
|
|
if (packet.error) {
|
|
let message;
|
|
if (packet.error && packet.message) {
|
|
message =
|
|
"Protocol error (" + packet.error + "): " + packet.message;
|
|
} else {
|
|
message = packet.error;
|
|
}
|
|
message += " from: " + this.actorID;
|
|
if (packet.fileName) {
|
|
const { fileName, columnNumber, lineNumber } = packet;
|
|
message += ` (${fileName}:${lineNumber}:${columnNumber})`;
|
|
}
|
|
const packetError = new Error(message);
|
|
|
|
// Pass the packets on the exception object to convey them to AppErrorBoundary
|
|
packetError.serverPacket = packet;
|
|
packetError.clientPacket = clientPacket;
|
|
|
|
deferred.reject(packetError);
|
|
} else {
|
|
deferred.resolve(packet);
|
|
}
|
|
},
|
|
stack,
|
|
"DevTools RDP"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* DevToolsClient will notify Fronts about bulk packet via this method.
|
|
*/
|
|
onBulkPacket(packet) {
|
|
// We can actually consider the bulk packet as a regular packet.
|
|
this.onPacket(packet);
|
|
}
|
|
|
|
hasRequests() {
|
|
return !!this._requests.length;
|
|
}
|
|
|
|
/**
|
|
* Wait for all current requests from this front to settle. This is especially useful
|
|
* for tests and other utility environments that may not have events or mechanisms to
|
|
* await the completion of requests without this utility.
|
|
*
|
|
* @return Promise
|
|
* Resolved when all requests have settled.
|
|
*/
|
|
waitForRequestsToSettle() {
|
|
return settleAll(this._requests.map(({ deferred }) => deferred.promise));
|
|
}
|
|
}
|
|
|
|
exports.Front = Front;
|