Actor.js (9493B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 var { Pool } = require("resource://devtools/shared/protocol/Pool.js"); 8 9 // Lazy load this symbol in order to prevent a dependency cycle between Actor and types. 10 loader.lazyRequireGetter( 11 this, 12 "BULK_RESPONSE", 13 "resource://devtools/shared/protocol/types.js", 14 true 15 ); 16 17 /** 18 * Keep track of which actorSpecs have been created. If a replica of a spec 19 * is created, it can be caught, and specs which inherit from other specs will 20 * not overwrite eachother. 21 */ 22 var actorSpecs = new WeakMap(); 23 24 exports.actorSpecs = actorSpecs; 25 26 /** 27 * An actor in the actor tree. 28 * 29 * @param optional conn 30 * Either a DevToolsServerConnection or a DevToolsClient. Must have 31 * addActorPool, removeActorPool, and poolFor. 32 * conn can be null if the subclass provides a conn property. 33 * @class 34 */ 35 36 class Actor extends Pool { 37 constructor(conn, spec) { 38 super(conn); 39 40 this.typeName = spec.typeName; 41 42 // Will contain the actor's ID 43 this.actorID = null; 44 45 // Ensure computing requestTypes only one time per class 46 const proto = Object.getPrototypeOf(this); 47 if (!proto.requestTypes) { 48 proto.requestTypes = generateRequestTypes(spec); 49 } 50 51 // Forward events to the connection. 52 if (spec.events) { 53 for (const [name, request] of spec.events.entries()) { 54 this.on(name, (...args) => { 55 this._sendEvent(name, request, ...args); 56 }); 57 } 58 } 59 } 60 61 toString() { 62 return "[Actor " + this.typeName + "/" + this.actorID + "]"; 63 } 64 65 _sendEvent(name, request, ...args) { 66 if (this.isDestroyed()) { 67 console.error( 68 `Tried to send a '${name}' event on an already destroyed actor` + 69 ` '${this.typeName}'` 70 ); 71 return; 72 } 73 let packet; 74 try { 75 packet = request.write(args, this); 76 } catch (ex) { 77 console.error("Error sending event: " + name); 78 throw ex; 79 } 80 packet.from = packet.from || this.actorID; 81 this.conn.send(packet); 82 83 // This can really be a hot path, even computing the marker label can 84 // have some performance impact. 85 // Guard against missing `Services.profiler` because Services is mocked to 86 // an empty object in the worker loader. 87 if (Services.profiler?.IsActive()) { 88 ChromeUtils.addProfilerMarker( 89 "DevTools:RDP Actor", 90 null, 91 `${this.typeName}.${name}` 92 ); 93 } 94 } 95 96 destroy() { 97 super.destroy(); 98 this.actorID = null; 99 this._isDestroyed = true; 100 } 101 102 /** 103 * Override this method in subclasses to serialize the actor. 104 * 105 * @returns A jsonable object. 106 */ 107 form() { 108 return { actor: this.actorID }; 109 } 110 111 writeError(error, typeName, method) { 112 console.error( 113 `Error while calling actor '${typeName}'s method '${method}'`, 114 error.message || error 115 ); 116 // Also log the error object as-is in order to log the server side stack 117 // nicely in the console, while the previous log will log the client side stack only. 118 if (error.stack) { 119 console.error(error); 120 } 121 122 // Do not try to send the error if the actor is destroyed 123 // as the connection is probably also destroyed and may throw. 124 if (this.isDestroyed()) { 125 return; 126 } 127 128 this.conn.send({ 129 from: this.actorID, 130 // error.error -> errors created using the throwError() helper 131 // error.name -> errors created using `new Error` or Components.exception 132 // typeof(error)=="string" -> a method thrown like this `throw "a string"` 133 error: 134 error.error || 135 error.name || 136 (typeof error == "string" ? error : "unknownError"), 137 message: error.message, 138 // error.fileName -> regular Error instances 139 // error.filename -> errors created using Components.exception 140 fileName: error.fileName || error.filename, 141 lineNumber: error.lineNumber, 142 columnNumber: error.columnNumber, 143 // Also pass the whole stack as string. 144 // 145 // "out of memory" string may be thrown by SpiderMonkey, 146 // in which case getLastOOMStackTrace can return a last resort stack as a string. 147 // https://searchfox.org/firefox-main/rev/33bba5cfe4a89dda0ee07fa9fbac578353713fd3/js/src/vm/JSContext.cpp#296-297 148 stack: 149 error == "out of memory" 150 ? ChromeUtils.getLastOOMStackTrace() 151 : error.stack, 152 }); 153 } 154 155 _queueResponse(create) { 156 const pending = this._pendingResponse || Promise.resolve(null); 157 const response = create(pending); 158 this._pendingResponse = response; 159 } 160 161 /** 162 * Throw an error with the passed message and attach an `error` property to the Error 163 * object so it can be consumed by the writeError function. 164 * 165 * @param {string} error: A string (usually a single word serving as an id) that will 166 * be assign to error.error. 167 * @param {string} message: The string that will be passed to the Error constructor. 168 * @throws This always throw. 169 */ 170 throwError(error, message) { 171 const err = new Error(message); 172 err.error = error; 173 throw err; 174 } 175 } 176 177 exports.Actor = Actor; 178 179 /** 180 * Generate the "requestTypes" object used by DevToolsServerConnection to implement RDP. 181 * When a RDP packet is received for calling an actor method, this lookup for 182 * the method name in this object and call the function holded on this attribute. 183 * 184 * @param {object} actorSpec 185 * The procotol-js actor specific coming from devtools/shared/specs/*.js files 186 * This describes the types for methods and events implemented by all actors. 187 * @return {object} requestTypes 188 * An object where attributes are actor method names 189 * and values are function implementing these methods. 190 * These methods receive a RDP Packet (JSON-serializable object) and a DevToolsServerConnection. 191 * We expect them to return a promise that reserves with the response object 192 * to send back to the client (JSON-serializable object). 193 */ 194 var generateRequestTypes = function (actorSpec) { 195 // Generate request handlers for each method definition 196 const requestTypes = Object.create(null); 197 actorSpec.methods.forEach(spec => { 198 const handler = function (packet, conn) { 199 try { 200 const startTime = isWorker ? null : ChromeUtils.now(); 201 let args; 202 try { 203 args = spec.request.read(packet, this); 204 } catch (ex) { 205 console.error("Error reading request: " + packet.type); 206 throw ex; 207 } 208 209 if (!this[spec.name]) { 210 throw new Error( 211 `Spec for '${actorSpec.typeName}' specifies a '${spec.name}'` + 212 ` method that isn't implemented by the actor` 213 ); 214 } 215 216 // If this method is flaged to be returning a bulk response, 217 // expose a method as last argument which will initiate the bulk response 218 // and return a promise resolving to a StreamCopier instance. 219 const isBulkResponse = spec.response.template === BULK_RESPONSE; 220 if (isBulkResponse) { 221 args.push(length => { 222 return this.conn.startBulkSend({ 223 actor: this.actorID, 224 length, 225 }); 226 }); 227 } 228 const ret = this[spec.name].apply(this, args); 229 230 const sendReturn = retToSend => { 231 if (spec.oneway) { 232 // No need to send a response. 233 return; 234 } 235 if (isBulkResponse) { 236 if (retToSend) { 237 throw new Actor( 238 `Actor method '${this.typeName}.${spec.name}' is supposed to return a bulk response, but returned some value.` 239 ); 240 } 241 // Bulk response are one-way requests and are not replying any JSON packet. 242 return; 243 } 244 if (this.isDestroyed()) { 245 console.error( 246 `Tried to send a '${spec.name}' method reply on an already destroyed actor` + 247 ` '${this.typeName}'` 248 ); 249 return; 250 } 251 252 let response; 253 try { 254 response = spec.response.write(retToSend, this); 255 } catch (ex) { 256 console.error("Error writing response to: " + spec.name); 257 throw ex; 258 } 259 response.from = this.actorID; 260 // If spec.release has been specified, destroy the object. 261 if (spec.release) { 262 try { 263 this.destroy(); 264 } catch (e) { 265 this.writeError(e, actorSpec.typeName, spec.name); 266 return; 267 } 268 } 269 270 conn.send(response); 271 272 ChromeUtils.addProfilerMarker( 273 "DevTools:RDP Actor", 274 startTime, 275 `${actorSpec.typeName}:${spec.name}()` 276 ); 277 }; 278 279 this._queueResponse(p => { 280 return p 281 .then(() => ret) 282 .then(sendReturn) 283 .catch(e => this.writeError(e, actorSpec.typeName, spec.name)); 284 }); 285 } catch (e) { 286 this._queueResponse(p => { 287 return p.then(() => 288 this.writeError(e, actorSpec.typeName, spec.name) 289 ); 290 }); 291 } 292 }; 293 294 requestTypes[spec.request.type] = handler; 295 }); 296 297 return requestTypes; 298 }; 299 exports.generateRequestTypes = generateRequestTypes;