browser_resources_sources.js (14698B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test the ResourceCommand API around SOURCE. 7 // 8 // We cover each Spidermonkey Debugger Source's `introductionType`: 9 // https://searchfox.org/mozilla-central/rev/4c184ca81b28f1ccffbfd08f465709b95bcb4aa1/js/src/doc/Debugger/Debugger.Source.md#172-213 10 // 11 // And especially cover sources being GC-ed before DevTools are opened 12 // which are later recreated by `ThreadActor.resurrectSource`. 13 14 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js"); 15 16 const TEST_URL = URL_ROOT_SSL + "sources.html"; 17 18 const TEST_JS_URL = URL_ROOT_SSL + "sources.js"; 19 const TEST_WORKER_URL = URL_ROOT_SSL + "worker-sources.js"; 20 const TEST_SW_URL = URL_ROOT_SSL + "service-worker-sources.js"; 21 22 async function getExpectedResources(ignoreUnresurrectedSources = false) { 23 const htmlRequest = await fetch(TEST_URL); 24 const htmlContent = await htmlRequest.text(); 25 26 // First list sources that aren't GC-ed, or that the thread actor is able to resurrect 27 const expectedSources = [ 28 { 29 description: "eval", 30 sourceForm: { 31 introductionType: "eval", 32 sourceMapBaseURL: TEST_URL, 33 url: null, 34 isBlackBoxed: false, 35 sourceMapURL: null, 36 extensionName: null, 37 isInlineSource: false, 38 }, 39 sourceContent: { 40 contentType: "text/javascript", 41 source: "this.global = function evalFunction() {}", 42 }, 43 }, 44 { 45 description: "new Function()", 46 sourceForm: { 47 introductionType: "Function", 48 sourceMapBaseURL: TEST_URL, 49 url: null, 50 isBlackBoxed: false, 51 sourceMapURL: null, 52 extensionName: null, 53 isInlineSource: false, 54 }, 55 sourceContent: { 56 contentType: "text/javascript", 57 source: "function anonymous(\n) {\nreturn 42;\n}", 58 }, 59 }, 60 { 61 description: "Event Handler", 62 sourceForm: { 63 introductionType: "eventHandler", 64 sourceMapBaseURL: TEST_URL, 65 url: null, 66 isBlackBoxed: false, 67 sourceMapURL: null, 68 extensionName: null, 69 isInlineSource: false, 70 }, 71 sourceContent: { 72 contentType: "text/javascript", 73 source: "console.log('link')", 74 }, 75 }, 76 { 77 description: "inline JS inserted at runtime", 78 sourceForm: { 79 introductionType: "scriptElement", // This is an injectedScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form() 80 sourceMapBaseURL: TEST_URL, 81 url: null, 82 isBlackBoxed: false, 83 sourceMapURL: null, 84 extensionName: null, 85 isInlineSource: false, 86 }, 87 sourceContent: { 88 contentType: "text/javascript", 89 source: "console.log('inline-script')", 90 }, 91 }, 92 { 93 description: "inline JS", 94 sourceForm: { 95 introductionType: "scriptElement", // This is an inlineScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form() 96 sourceMapBaseURL: TEST_URL, 97 url: TEST_URL, 98 isBlackBoxed: false, 99 sourceMapURL: null, 100 extensionName: null, 101 isInlineSource: true, 102 }, 103 sourceContent: { 104 contentType: "text/html", 105 source: htmlContent, 106 }, 107 }, 108 { 109 description: "worker script", 110 sourceForm: { 111 introductionType: undefined, 112 sourceMapBaseURL: TEST_WORKER_URL, 113 url: TEST_WORKER_URL, 114 isBlackBoxed: false, 115 sourceMapURL: null, 116 extensionName: null, 117 isInlineSource: false, 118 }, 119 sourceContent: { 120 contentType: "text/javascript", 121 source: "/* eslint-disable */\nfunction workerSource() {}\n", 122 }, 123 }, 124 { 125 description: "service worker script", 126 sourceForm: { 127 introductionType: undefined, 128 sourceMapBaseURL: TEST_SW_URL, 129 url: TEST_SW_URL, 130 isBlackBoxed: false, 131 sourceMapURL: null, 132 extensionName: null, 133 isInlineSource: false, 134 }, 135 sourceContent: { 136 contentType: "text/javascript", 137 source: "/* eslint-disable */\nfunction serviceWorkerSource() {}\n", 138 }, 139 }, 140 { 141 description: "independent js file", 142 sourceForm: { 143 introductionType: "scriptElement", // This is an srcScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form() 144 sourceMapBaseURL: TEST_JS_URL, 145 url: TEST_JS_URL, 146 isBlackBoxed: false, 147 sourceMapURL: null, 148 extensionName: null, 149 isInlineSource: false, 150 }, 151 sourceContent: { 152 contentType: "text/javascript", 153 source: "/* eslint-disable */\nfunction scriptSource() {}\n", 154 }, 155 }, 156 { 157 description: "DOM Timer", 158 sourceForm: { 159 introductionType: "domTimer", 160 sourceMapBaseURL: TEST_URL, 161 url: null, 162 isBlackBoxed: false, 163 sourceMapURL: null, 164 extensionName: null, 165 isInlineSource: false, 166 }, 167 sourceContent: { 168 contentType: "text/javascript", 169 /* the domTimer is prefixed by many empty lines in order to be positioned at the same line 170 as in the HTML file where setTimeout is called. 171 This is probably done by SourceActor.actualText(). 172 So the array size here, should be updated to match the line number of setTimeout call */ 173 source: new Array(39).join("\n") + `console.log("timeout")`, 174 }, 175 }, 176 ]; 177 178 // Now list the sources that could be GC-ed for which the thread actor isn't able to resurrect. 179 // This is the sources that we can't assert when we fetch sources after the page is already loaded. 180 const unresurrectedSources = [ 181 { 182 description: "javascript URL", 183 sourceForm: { 184 introductionType: "javascriptURL", 185 sourceMapBaseURL: "about:blank", 186 url: null, 187 isBlackBoxed: false, 188 sourceMapURL: null, 189 extensionName: null, 190 isInlineSource: false, 191 }, 192 sourceContent: { 193 contentType: "text/javascript", 194 source: "'666'", 195 }, 196 }, 197 { 198 description: "srcdoc attribute on iframes #1", 199 sourceForm: { 200 introductionType: "scriptElement", 201 // We do not assert url/sourceMapBaseURL as it includes the Debugger.Source.id 202 // which is random 203 isBlackBoxed: false, 204 sourceMapURL: null, 205 extensionName: null, 206 isInlineSource: false, 207 }, 208 sourceContent: { 209 contentType: "text/javascript", 210 source: "console.log('srcdoc')", 211 }, 212 }, 213 { 214 description: "srcdoc attribute on iframes #2", 215 sourceForm: { 216 introductionType: "scriptElement", 217 // We do not assert url/sourceMapBaseURL as it includes the Debugger.Source.id 218 // which is random 219 isBlackBoxed: false, 220 sourceMapURL: null, 221 extensionName: null, 222 isInlineSource: false, 223 }, 224 sourceContent: { 225 contentType: "text/javascript", 226 source: "console.log('srcdoc 2')", 227 }, 228 }, 229 ]; 230 231 if (ignoreUnresurrectedSources) { 232 return expectedSources; 233 } 234 return expectedSources.concat(unresurrectedSources); 235 } 236 237 add_task(async function testSourcesOnload() { 238 // Load an blank document first, in order to load the test page only once we already 239 // started watching for sources 240 const tab = await addTab("about:blank"); 241 242 const commands = await CommandsFactory.forTab(tab); 243 const { targetCommand, resourceCommand } = commands; 244 245 // Force the target list to cover workers and debug all the targets 246 targetCommand.listenForWorkers = true; 247 targetCommand.listenForServiceWorkers = true; 248 await targetCommand.startListening(); 249 250 info("Check already available resources"); 251 const availableResources = []; 252 await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], { 253 onAvailable: resources => availableResources.push(...resources), 254 }); 255 256 const promiseLoad = BrowserTestUtils.browserLoaded( 257 gBrowser.selectedBrowser, 258 false, 259 TEST_URL 260 ); 261 BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, TEST_URL); 262 await promiseLoad; 263 264 // Some sources may be created after the document is done loading (like eventHandler usecase) 265 // so we may be received *after* watchResource resolved 266 const expectedResources = await getExpectedResources(); 267 await waitFor( 268 () => availableResources.length >= expectedResources.length, 269 "Got all the sources" 270 ); 271 272 await assertResources(availableResources, expectedResources); 273 274 await commands.destroy(); 275 276 await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { 277 // registrationPromise is set by the test page. 278 const registration = await content.wrappedJSObject.registrationPromise; 279 registration.unregister(); 280 }); 281 }); 282 283 // Bug 1767772: Skipped via add_task(...).skip() for very frequent intermittent 284 // failures. 285 add_task(async function testGarbagedCollectedSources() { 286 info( 287 "Assert SOURCES on an already loaded page with some sources that have been GC-ed" 288 ); 289 const tab = await addTab(TEST_URL); 290 291 info("Force some GC to free some sources"); 292 // GC are not always guaranteed to be effective in one call, 293 // so increase our chances of effectively free objects by doing two with a pause between them. 294 await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { 295 Cu.forceGC(); 296 Cu.forceCC(); 297 }); 298 await wait(500); 299 await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { 300 Cu.forceGC(); 301 Cu.forceCC(); 302 }); 303 304 const commands = await CommandsFactory.forTab(tab); 305 const { targetCommand, resourceCommand } = commands; 306 307 // Force the target list to cover workers and debug all the targets 308 targetCommand.listenForWorkers = true; 309 targetCommand.listenForServiceWorkers = true; 310 await targetCommand.startListening(); 311 312 info("Check already available resources"); 313 const availableResources = []; 314 await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], { 315 onAvailable: resources => availableResources.push(...resources), 316 }); 317 318 // Some sources may be created after the document is done loading (like eventHandler usecase) 319 // so we may be received *after* watchResource resolved 320 const expectedResources = await getExpectedResources(true); 321 await waitFor( 322 () => availableResources.length >= expectedResources.length, 323 "Got all the sources" 324 ); 325 326 await assertResources(availableResources, expectedResources); 327 328 await commands.destroy(); 329 330 await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { 331 // registrationPromise is set by the test page. 332 const registration = await content.wrappedJSObject.registrationPromise; 333 registration.unregister(); 334 }); 335 }).skip(); 336 337 /** 338 * Assert that evaluating sources for a new global, in the parent process 339 * using the shared system principal will spawn SOURCE resources. 340 * 341 * For this we use a special `commands` which replicate what browser console 342 * and toolbox use. 343 */ 344 add_task(async function testParentProcessPrivilegedSources() { 345 // Use a custom loader + server + client in order to spawn the server 346 // in a distinct system compartment, so that it can see the system compartment 347 // sandbox we are about to create in this test 348 const client = await CommandsFactory.spawnClientToDebugSystemPrincipal(); 349 350 const commands = await CommandsFactory.forMainProcess({ client }); 351 await commands.targetCommand.startListening(); 352 const { resourceCommand } = commands; 353 354 info("Check already available resources"); 355 const availableResources = []; 356 await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], { 357 onAvailable: resources => availableResources.push(...resources), 358 }); 359 ok( 360 !!availableResources.length, 361 "We get many sources reported from a multiprocess command" 362 ); 363 364 // Clear the list of sources 365 availableResources.length = 0; 366 367 // Force the creation of a new privileged source 368 const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance( 369 Ci.nsIPrincipal 370 ); 371 const sandbox = Cu.Sandbox(systemPrincipal); 372 Cu.evalInSandbox("function foo() {}", sandbox, null, "http://foo.com"); 373 374 info("Wait for the sandbox source"); 375 await waitFor(() => { 376 return availableResources.some( 377 resource => resource.url == "http://foo.com/" 378 ); 379 }); 380 381 const expectedResources = [ 382 { 383 description: "privileged sandbox script", 384 sourceForm: { 385 introductionType: undefined, 386 sourceMapBaseURL: "http://foo.com/", 387 url: "http://foo.com/", 388 isBlackBoxed: false, 389 sourceMapURL: null, 390 extensionName: null, 391 isInlineSource: false, 392 }, 393 sourceContent: { 394 contentType: "text/javascript", 395 source: "function foo() {}", 396 }, 397 }, 398 ]; 399 const matchingResource = availableResources.filter(resource => 400 resource.url.includes("http://foo.com") 401 ); 402 await assertResources(matchingResource, expectedResources); 403 404 await commands.destroy(); 405 }); 406 407 async function assertResources(resources, expected) { 408 is( 409 resources.length, 410 expected.length, 411 "Length of existing resources is correct at initial" 412 ); 413 for (let i = 0; i < resources.length; i++) { 414 await assertResource(resources[i], expected); 415 } 416 } 417 418 async function assertResource(source, expected) { 419 is( 420 source.resourceType, 421 ResourceCommand.TYPES.SOURCE, 422 "Resource type is correct" 423 ); 424 425 const threadFront = await source.targetFront.getFront("thread"); 426 // `source` is SourceActor's form() 427 // so try to instantiate the related SourceFront: 428 const sourceFront = threadFront.source(source); 429 // then fetch source content 430 const sourceContent = await sourceFront.source(); 431 432 // Order of sources is random, so we have to find the best expected resource. 433 // The only unique attribute is the JS Source text content. 434 const matchingExpected = expected.find(res => { 435 return res.sourceContent.source == sourceContent.source; 436 }); 437 ok( 438 matchingExpected, 439 `This source was expected with source content being "${sourceContent.source}"` 440 ); 441 info(`Found "#${matchingExpected.description}"`); 442 assertObject( 443 sourceContent, 444 matchingExpected.sourceContent, 445 matchingExpected.description 446 ); 447 448 assertObject( 449 source, 450 matchingExpected.sourceForm, 451 matchingExpected.description 452 ); 453 } 454 455 function assertObject(object, expected, description) { 456 for (const field in expected) { 457 is( 458 object[field], 459 expected[field], 460 `The value of ${field} is correct for "#${description}"` 461 ); 462 } 463 }