1 /* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.TargetAwareObject} 34 * @param {!WebInspector.Target} target 35 */ 36 WebInspector.RuntimeModel = function(target) 37 { 38 WebInspector.TargetAwareObject.call(this, target); 39 40 this._debuggerModel = target.debuggerModel; 41 this._agent = target.runtimeAgent(); 42 this.target().registerRuntimeDispatcher(new WebInspector.RuntimeDispatcher(this)); 43 this._agent.enable(); 44 /** 45 * @type {!Object.<number, !WebInspector.ExecutionContext>} 46 */ 47 this._executionContextById = {}; 48 } 49 50 WebInspector.RuntimeModel.Events = { 51 ExecutionContextCreated: "ExecutionContextCreated", 52 ExecutionContextDestroyed: "ExecutionContextDestroyed", 53 } 54 55 WebInspector.RuntimeModel.prototype = { 56 57 /** 58 * @return {!Array.<!WebInspector.ExecutionContext>} 59 */ 60 executionContexts: function() 61 { 62 return Object.values(this._executionContextById); 63 }, 64 65 /** 66 * @param {!RuntimeAgent.ExecutionContextDescription} context 67 */ 68 _executionContextCreated: function(context) 69 { 70 var executionContext = new WebInspector.ExecutionContext(this.target(), context.id, context.name, context.isPageContext, context.frameId); 71 this._executionContextById[executionContext.id] = executionContext; 72 this.dispatchEventToListeners(WebInspector.RuntimeModel.Events.ExecutionContextCreated, executionContext); 73 }, 74 75 /** 76 * @param {number} executionContextId 77 */ 78 _executionContextDestroyed: function(executionContextId) 79 { 80 var executionContext = this._executionContextById[executionContextId]; 81 delete this._executionContextById[executionContextId]; 82 this.dispatchEventToListeners(WebInspector.RuntimeModel.Events.ExecutionContextDestroyed, executionContext); 83 }, 84 85 _executionContextsCleared: function() 86 { 87 var contexts = this.executionContexts(); 88 this._executionContextById = {}; 89 for (var i = 0; i < contexts.length; ++i) 90 this.dispatchEventToListeners(WebInspector.RuntimeModel.Events.ExecutionContextDestroyed, contexts[i]); 91 }, 92 93 /** 94 * @param {!RuntimeAgent.RemoteObject} payload 95 * @return {!WebInspector.RemoteObject} 96 */ 97 createRemoteObject: function(payload) 98 { 99 console.assert(typeof payload === "object", "Remote object payload should only be an object"); 100 return new WebInspector.RemoteObjectImpl(this.target(), payload.objectId, payload.type, payload.subtype, payload.value, payload.description, payload.preview); 101 }, 102 103 /** 104 * @param {!RuntimeAgent.RemoteObject} payload 105 * @param {!WebInspector.ScopeRef} scopeRef 106 * @return {!WebInspector.RemoteObject} 107 */ 108 createScopeRemoteObject: function(payload, scopeRef) 109 { 110 return new WebInspector.ScopeRemoteObject(this.target(), payload.objectId, scopeRef, payload.type, payload.subtype, payload.value, payload.description, payload.preview); 111 }, 112 113 /** 114 * @param {number|string|boolean} value 115 * @return {!WebInspector.RemoteObject} 116 */ 117 createRemoteObjectFromPrimitiveValue: function(value) 118 { 119 return new WebInspector.RemoteObjectImpl(this.target(), undefined, typeof value, undefined, value); 120 }, 121 122 /** 123 * @param {string} name 124 * @param {number|string|boolean} value 125 * @return {!WebInspector.RemoteObjectProperty} 126 */ 127 createRemotePropertyFromPrimitiveValue: function(name, value) 128 { 129 return new WebInspector.RemoteObjectProperty(name, this.createRemoteObjectFromPrimitiveValue(value)); 130 }, 131 132 __proto__: WebInspector.TargetAwareObject.prototype 133 } 134 135 /** 136 * @constructor 137 * @implements {RuntimeAgent.Dispatcher} 138 * @param {!WebInspector.RuntimeModel} runtimeModel 139 */ 140 WebInspector.RuntimeDispatcher = function(runtimeModel) 141 { 142 this._runtimeModel = runtimeModel; 143 } 144 145 WebInspector.RuntimeDispatcher.prototype = { 146 executionContextCreated: function(context) 147 { 148 this._runtimeModel._executionContextCreated(context); 149 }, 150 151 executionContextDestroyed: function(executionContextId) 152 { 153 this._runtimeModel._executionContextDestroyed(executionContextId); 154 }, 155 156 executionContextsCleared: function() 157 { 158 this._runtimeModel._executionContextsCleared(); 159 } 160 161 } 162 163 /** 164 * @constructor 165 * @extends {WebInspector.TargetAware} 166 * @param {!WebInspector.Target} target 167 * @param {number|undefined} id 168 * @param {string} name 169 * @param {boolean} isPageContext 170 * @param {string=} frameId 171 */ 172 WebInspector.ExecutionContext = function(target, id, name, isPageContext, frameId) 173 { 174 WebInspector.TargetAware.call(this, target); 175 this.id = id; 176 this.name = (isPageContext && !name) ? "<page context>" : name; 177 this.isMainWorldContext = isPageContext; 178 this._debuggerModel = target.debuggerModel; 179 this.frameId = frameId; 180 } 181 182 /** 183 * @param {!WebInspector.ExecutionContext} a 184 * @param {!WebInspector.ExecutionContext} b 185 * @return {number} 186 */ 187 WebInspector.ExecutionContext.comparator = function(a, b) 188 { 189 // Main world context should always go first. 190 if (a.isMainWorldContext) 191 return -1; 192 if (b.isMainWorldContext) 193 return +1; 194 return a.name.localeCompare(b.name); 195 } 196 197 WebInspector.ExecutionContext.prototype = { 198 199 /** 200 * @param {string} expression 201 * @param {string} objectGroup 202 * @param {boolean} includeCommandLineAPI 203 * @param {boolean} doNotPauseOnExceptionsAndMuteConsole 204 * @param {boolean} returnByValue 205 * @param {boolean} generatePreview 206 * @param {function(?WebInspector.RemoteObject, boolean, ?RuntimeAgent.RemoteObject=)} callback 207 */ 208 evaluate: function(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, callback) 209 { 210 //FIXME: It will be moved to separate ExecutionContext 211 if (this._debuggerModel.selectedCallFrame()) { 212 this._debuggerModel.evaluateOnSelectedCallFrame(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, callback); 213 return; 214 } 215 216 if (!expression) { 217 // There is no expression, so the completion should happen against global properties. 218 expression = "this"; 219 } 220 221 /** 222 * @this {WebInspector.ExecutionContext} 223 * @param {?Protocol.Error} error 224 * @param {!RuntimeAgent.RemoteObject} result 225 * @param {boolean=} wasThrown 226 */ 227 function evalCallback(error, result, wasThrown) 228 { 229 if (error) { 230 callback(null, false); 231 return; 232 } 233 234 if (returnByValue) 235 callback(null, !!wasThrown, wasThrown ? null : result); 236 else 237 callback(this.target().runtimeModel.createRemoteObject(result), !!wasThrown); 238 } 239 this.target().runtimeAgent().evaluate(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, this.id, returnByValue, generatePreview, evalCallback.bind(this)); 240 }, 241 242 /** 243 * @param {string} expressionString 244 * @param {string} prefix 245 * @param {boolean} force 246 * @param {function(!Array.<string>, number=)} completionsReadyCallback 247 */ 248 completionsForExpression: function(expressionString, prefix, force, completionsReadyCallback) 249 { 250 var lastIndex = expressionString.length - 1; 251 252 var dotNotation = (expressionString[lastIndex] === "."); 253 var bracketNotation = (expressionString[lastIndex] === "["); 254 255 if (dotNotation || bracketNotation) 256 expressionString = expressionString.substr(0, lastIndex); 257 258 if (expressionString && parseInt(expressionString, 10) == expressionString) { 259 // User is entering float value, do not suggest anything. 260 completionsReadyCallback([]); 261 return; 262 } 263 264 if (!prefix && !expressionString && !force) { 265 completionsReadyCallback([]); 266 return; 267 } 268 269 if (!expressionString && this._debuggerModel.selectedCallFrame()) 270 this._debuggerModel.getSelectedCallFrameVariables(receivedPropertyNames.bind(this)); 271 else 272 this.evaluate(expressionString, "completion", true, true, false, false, evaluated.bind(this)); 273 274 /** 275 * @this {WebInspector.ExecutionContext} 276 */ 277 function evaluated(result, wasThrown) 278 { 279 if (!result || wasThrown) { 280 completionsReadyCallback([]); 281 return; 282 } 283 284 /** 285 * @param {string} primitiveType 286 * @suppressReceiverCheck 287 * @this {WebInspector.ExecutionContext} 288 */ 289 function getCompletions(primitiveType) 290 { 291 var object; 292 if (primitiveType === "string") 293 object = new String(""); 294 else if (primitiveType === "number") 295 object = new Number(0); 296 else if (primitiveType === "boolean") 297 object = new Boolean(false); 298 else 299 object = this; 300 301 var resultSet = {}; 302 for (var o = object; o; o = o.__proto__) { 303 try { 304 var names = Object.getOwnPropertyNames(o); 305 for (var i = 0; i < names.length; ++i) 306 resultSet[names[i]] = true; 307 } catch (e) { 308 } 309 } 310 return resultSet; 311 } 312 313 if (result.type === "object" || result.type === "function") 314 result.callFunctionJSON(getCompletions, undefined, receivedPropertyNames.bind(this)); 315 else if (result.type === "string" || result.type === "number" || result.type === "boolean") 316 this.evaluate("(" + getCompletions + ")(\"" + result.type + "\")", "completion", false, true, true, false, receivedPropertyNamesFromEval.bind(this)); 317 } 318 319 /** 320 * @param {?WebInspector.RemoteObject} notRelevant 321 * @param {boolean} wasThrown 322 * @param {?RuntimeAgent.RemoteObject=} result 323 * @this {WebInspector.ExecutionContext} 324 */ 325 function receivedPropertyNamesFromEval(notRelevant, wasThrown, result) 326 { 327 if (result && !wasThrown) 328 receivedPropertyNames.call(this, result.value); 329 else 330 completionsReadyCallback([]); 331 } 332 333 /** 334 * @this {WebInspector.ExecutionContext} 335 */ 336 function receivedPropertyNames(propertyNames) 337 { 338 this.target().runtimeAgent().releaseObjectGroup("completion"); 339 if (!propertyNames) { 340 completionsReadyCallback([]); 341 return; 342 } 343 var includeCommandLineAPI = (!dotNotation && !bracketNotation); 344 if (includeCommandLineAPI) { 345 const commandLineAPI = ["dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear", 346 "getEventListeners", "debug", "undebug", "monitor", "unmonitor", "table", "$", "$$", "$x"]; 347 for (var i = 0; i < commandLineAPI.length; ++i) 348 propertyNames[commandLineAPI[i]] = true; 349 } 350 this._reportCompletions(completionsReadyCallback, dotNotation, bracketNotation, expressionString, prefix, Object.keys(propertyNames)); 351 } 352 }, 353 354 /** 355 * @param {function(!Array.<string>, number=)} completionsReadyCallback 356 * @param {boolean} dotNotation 357 * @param {boolean} bracketNotation 358 * @param {string} expressionString 359 * @param {string} prefix 360 * @param {!Array.<string>} properties 361 */ 362 _reportCompletions: function(completionsReadyCallback, dotNotation, bracketNotation, expressionString, prefix, properties) { 363 if (bracketNotation) { 364 if (prefix.length && prefix[0] === "'") 365 var quoteUsed = "'"; 366 else 367 var quoteUsed = "\""; 368 } 369 370 var results = []; 371 372 if (!expressionString) { 373 const keywords = ["break", "case", "catch", "continue", "default", "delete", "do", "else", "finally", "for", "function", "if", "in", 374 "instanceof", "new", "return", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with"]; 375 properties = properties.concat(keywords); 376 } 377 378 properties.sort(); 379 380 for (var i = 0; i < properties.length; ++i) { 381 var property = properties[i]; 382 383 // Assume that all non-ASCII characters are letters and thus can be used as part of identifier. 384 if (dotNotation && !/^[a-zA-Z_$\u008F-\uFFFF][a-zA-Z0-9_$\u008F-\uFFFF]*$/.test(property)) 385 continue; 386 387 if (bracketNotation) { 388 if (!/^[0-9]+$/.test(property)) 389 property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed; 390 property += "]"; 391 } 392 393 if (property.length < prefix.length) 394 continue; 395 if (prefix.length && !property.startsWith(prefix)) 396 continue; 397 398 results.push(property); 399 } 400 completionsReadyCallback(results); 401 }, 402 403 __proto__: WebInspector.TargetAware.prototype 404 } 405 406 /** 407 * @type {!WebInspector.RuntimeModel} 408 */ 409 WebInspector.runtimeModel; 410