Created
January 21, 2020 21:37
-
-
Save andreastt/393d79092d383d8273d83c2022d838c6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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