Skip to content

Instantly share code, notes, and snippets.

@andreastt
Created January 21, 2020 21:37
Show Gist options
  • Save andreastt/393d79092d383d8273d83c2022d838c6 to your computer and use it in GitHub Desktop.
Save andreastt/393d79092d383d8273d83c2022d838c6 to your computer and use it in GitHub Desktop.
/* 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 EXPORTED_SYMBOLS = ["IPC"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"UUIDGenerator",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator"
);
/*
IPC assists with transporting complex objects across process
boundaries.
It is designed to be used in conjunction with nsIMessageSender, but
can theoretically be used with any transport mechanism that supports
JSON.
The values must themselves be JSON serializable, with the exception
of Error and Exception prototypes. It is the intention to expand
this to cover other useful, but not JSON serializable, complex
objects in the future.
If an object implements a toJSON() function, its return value is
used instead. The same considerations and limitations as for direct
input values applies equally to these return values.
Serialized values can subsequently be reconstructed with
IPC.deserialize(). Deserialized complex objects will not be
value-identical to the original objects:
const complexObject = {};
const ser = IPC.serialize(complexObject);
const de = IPC.deserialize(ser);
dump(ser === de); // false
The serialization protocol is consciously quite similar to the way
remote objects, as defined in ./domains/content/runtime/ExecutionContext.jsm,
are marshaled. The notable difference is that objects and arrays
are recursively serialized instead of lazily accessed via a reference
cache. This means the module should be used sparringly when dealing
with big objects.
A possible future extension might be to optionally support passing
references to complex objects. Because consumers of this module
are JS based, it might be conceivable to use proxies to hide the
own property access, of course at the expense of increased message
manager chatter. It would also address some of the ping-pong
problems described by chromedriver developers regarding peeking
into arrays.
*/
/** Serialize value for transport on a message manager channel. */
function serialize(obj) {
if (obj && typeof obj.toJSON == "function") {
const unsafe = obj.toJSON();
return serialize(unsafe);
}
const type = typeof obj;
let subtype;
let value = obj;
if (obj === null) {
subtype = "null";
}
if (isCollection(obj)) {
// TODO(ato): deal with cyclic references?
subtype = "array";
value = [...obj].map(el => serialize(el));
}
switch (Object.prototype.toString.call(obj)) {
case "[object Error]":
case "[object Exception]":
subtype = "error";
value = SerializableError.fromError(obj);
break;
}
if (isObject(obj)) {
value = {};
for (const prop in obj) {
// TODO(ato): deal with cyclic references?
try {
value[prop] = serialize(obj[prop]);
} catch (e) {
if (e.result != Cr.NS_ERROR_NOT_IMPLEMENTED) {
throw e;
}
}
}
}
return { type, subtype, value };
}
/** Deserialize a JSON payload from a message manager channel. */
function deserialize(json) {
dump("deserialize json=" + JSON.stringify(json) + "\n");
if (!isObject(json)) {
throw new TypeError("IPC expected JSON Object: " + typeof json);
}
const { type, subtype, value } = json;
switch (type) {
case "boolean":
case "number":
case "string":
return value;
case "undefined":
return undefined;
case "object":
switch (subtype) {
case "null":
return null;
case "array":
return [...value].map(deserialize);
case "error":
return SerializableError.fromJSON(value);
default:
// object
const rv = {};
for (const prop in value) {
rv[prop] = deserialize(value[prop]);
}
return rv;
}
default:
// we could be less strict, but being strict may increase safety
throw new TypeError("Unknown serialization type: " + type);
}
}
const IPC = { serialize, deserialize };
class SerializableError {
constructor() {
// reconstructed errors are not value-identical
// so use a unique identifier for comparison
this.uuid = UUIDGenerator.generateUUID().toString();
this.name = null;
this.message = null;
this.extra = {};
this.cause = null;
this._stack = [];
}
get stack() {
return this._stack;
}
set stack(replacementStack) {
if (typeof replacementStack == "string") {
this._stack = replacementStack
.split("\n")
.map(frame => frame.trim())
.filter(frame => frame.length > 0);
} else if (Array.isArray(replacementStack)) {
this._stack = replacementStack;
} else if (replacementStack == null) {
this._stack = null;
} else {
throw new TypeError();
}
}
equals(other) {
return (
other && other instanceof SerializableError && this.uuid === other.uuid
);
}
// if it behaves and quacks like a duck...
toString() {
if (this.extra.result) {
const nsresult = `0x${this.extra.result.toString(16)} (${lookupNsResult(
this.extra.result
)})`;
return `[Exception... "${
this.message
}" nsresult: "${nsresult}" location: "${this.stack}" data: ${
this.extra.data ? "yes" : "no"
}]`;
}
if (this.message.length > 0) {
return `${this.name}: ${this.message}`;
}
return this.name;
}
toJSON() {
return {
uuid: this.uuid,
name: this.name,
message: this.message,
stack: this.stack,
cause: this.cause ? this.cause.toJSON() : null,
extra: this.extra,
};
}
static fromJSON(json) {
const err = new SerializableError();
err.uuid = json.uuid;
err.name = json.name;
err.message = json.message;
err.stack = json.stack;
err.cause = json.cause;
if (err.extra) {
err.extra.fileName = json.extra.fileName;
err.extra.lineNumber = json.extra.lineNumber;
err.extra.columnNumber = json.extra.columnNumber;
err.extra.result = json.extra.result;
err.extra.data = json.extra.data;
}
return err;
}
static fromError(other, { internal = false } = {}) {
// break out of serializing recursive cause chains
if (internal) {
return undefined;
}
const err = new SerializableError();
err.name = other.name;
err.message = other.message;
err.stack = other.stack;
if (other.cause) {
err.cause = SerializableError.fromError(other.cause, { internal: true });
}
err.extra.fileName = other.fileName || other.filename;
err.extra.lineNumber = other.lineNumber;
err.extra.columnNumber = other.columnNumber;
err.extra.result = other.result;
err.extra.data = other.data;
return err;
}
}
function isObject(thing) {
return Object.prototype.toString.call(thing) === "[object Object]";
}
function isCollection(seq) {
switch (Object.prototype.toString.call(seq)) {
case "[object Arguments]":
case "[object Array]":
case "[object FileList]":
case "[object HTMLAllCollection]":
case "[object HTMLCollection]":
case "[object HTMLFormControlsCollection]":
case "[object HTMLOptionsCollection]":
case "[object NodeList]":
return true;
default:
return false;
}
}
// Reverse lookup of NS results by their associated code.
//
// nsXPCComponents_Results does not interact well with JS iterator patterns.
// Using Object.entries(Components.results) or a similar modern technique
// will consequently not work. Fortunately we can still rely on good ol' for.
function lookupNsResult(code) {
for (const n in Cr) {
const c = Cr[n];
// skip QueryInterface()
if (typeof n != "string" || typeof c != "number") {
continue;
}
if (c === code) {
return n;
}
}
throw new TypeError("Unable to find NS result for code: " + code);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment