head.js (16461B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const utilityProcessTest = () => { 7 return Cc["@mozilla.org/utility-process-test;1"].createInstance( 8 Ci.nsIUtilityProcessTest 9 ); 10 }; 11 12 const kGenericUtilitySandbox = 0; 13 const kGenericUtilityActor = "unknown"; 14 15 // Start a generic utility process with the given array of utility actor names 16 // registered. 17 async function startUtilityProcess(actors = []) { 18 info("Start a UtilityProcess"); 19 return utilityProcessTest().startProcess(actors); 20 } 21 22 // Returns an array of process infos for utility processes of the given type 23 // or all utility processes if actor is not defined. 24 async function getUtilityProcesses(actor = undefined, options = {}) { 25 let procInfos = (await ChromeUtils.requestProcInfo()).children.filter(p => { 26 return ( 27 p.type === "utility" && 28 (actor == undefined || 29 p.utilityActors.find(a => a.actorName.startsWith(actor))) 30 ); 31 }); 32 33 if (!options?.quiet) { 34 info(`Utility process infos = ${JSON.stringify(procInfos)}`); 35 } 36 return procInfos; 37 } 38 39 async function tryGetUtilityPid(actor, options = {}) { 40 let process = await getUtilityProcesses(actor, options); 41 if (!options?.quiet) { 42 Assert.lessOrEqual( 43 process.length, 44 1, 45 `at most one ${actor} process exists` 46 ); 47 } 48 return process[0]?.pid; 49 } 50 51 async function checkUtilityExists(actor) { 52 info(`Looking for a running ${actor} utility process`); 53 const utilityPid = await tryGetUtilityPid(actor); 54 Assert.greater(utilityPid, 0, `Found ${actor} utility process ${utilityPid}`); 55 return utilityPid; 56 } 57 58 // "Cleanly stop" a utility process. This will never leave a crash dump file. 59 // preferKill will "kill" the process (e.g. SIGABRT) instead of using the 60 // UtilityProcessManager. 61 // To "crash" -- i.e. shutdown and generate a crash dump -- use 62 // crashSomeUtility(). 63 async function cleanUtilityProcessShutdown(actor, preferKill = false) { 64 info(`${preferKill ? "Kill" : "Clean shutdown"} Utility Process ${actor}`); 65 66 const utilityPid = await tryGetUtilityPid(actor); 67 Assert.notStrictEqual( 68 utilityPid, 69 undefined, 70 `Must have PID for ${actor} utility process` 71 ); 72 73 const utilityProcessGone = TestUtils.topicObserved( 74 "ipc:utility-shutdown", 75 (subject, data) => parseInt(data, 10) === utilityPid 76 ); 77 78 if (preferKill) { 79 SimpleTest.expectChildProcessCrash(); 80 info(`Kill Utility Process ${utilityPid}`); 81 const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( 82 Ci.nsIProcessToolsService 83 ); 84 ProcessTools.kill(utilityPid); 85 } else { 86 info(`Stopping Utility Process ${utilityPid}`); 87 await utilityProcessTest().stopProcess(actor); 88 } 89 90 let [subject, data] = await utilityProcessGone; 91 ok( 92 subject instanceof Ci.nsIPropertyBag2, 93 "Subject needs to be a nsIPropertyBag2 to clean up properly" 94 ); 95 is( 96 parseInt(data, 10), 97 utilityPid, 98 `Should match the crashed PID ${utilityPid} with ${data}` 99 ); 100 101 // Make sure the process is dead, otherwise there is a risk of race for 102 // writing leak logs 103 utilityProcessTest().noteIntentionalCrash(utilityPid); 104 105 ok(!subject.hasKey("dumpID"), "There should be no dumpID"); 106 } 107 108 async function killUtilityProcesses() { 109 let utilityProcesses = await getUtilityProcesses(); 110 for (const utilityProcess of utilityProcesses) { 111 for (const actor of utilityProcess.utilityActors) { 112 info(`Stopping ${actor.actorName} utility process`); 113 await cleanUtilityProcessShutdown(actor.actorName, /* preferKill */ true); 114 } 115 } 116 } 117 118 function audioTestData() { 119 return [ 120 { 121 src: "small-shot.ogg", 122 expectations: { 123 Android: { 124 process: "Utility Generic", 125 decoder: "ffvpx audio decoder", 126 }, 127 Linux: { 128 process: "Utility Generic", 129 decoder: "ffvpx audio decoder", 130 }, 131 WINNT: { 132 process: "Utility Generic", 133 decoder: "ffvpx audio decoder", 134 }, 135 Darwin: { 136 process: "Utility Generic", 137 decoder: "ffvpx audio decoder", 138 }, 139 }, 140 }, 141 { 142 src: "small-shot.mp3", 143 expectations: { 144 Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" }, 145 Linux: { 146 process: "Utility Generic", 147 decoder: "ffvpx audio decoder", 148 }, 149 WINNT: { 150 process: "Utility Generic", 151 decoder: "ffvpx audio decoder", 152 }, 153 Darwin: { 154 process: "Utility Generic", 155 decoder: "ffvpx audio decoder", 156 }, 157 }, 158 }, 159 { 160 src: "small-shot.m4a", 161 expectations: { 162 // Add Android after Bug 1934009 163 Linux: { 164 process: "Utility Generic", 165 decoder: "ffmpeg audio decoder", 166 }, 167 WINNT: { 168 process: "Utility WMF", 169 decoder: "wmf audio decoder", 170 }, 171 Darwin: { 172 process: "Utility AppleMedia", 173 decoder: "apple coremedia decoder", 174 }, 175 }, 176 }, 177 { 178 src: "small-shot.flac", 179 expectations: { 180 Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" }, 181 Linux: { 182 process: "Utility Generic", 183 decoder: "ffvpx audio decoder", 184 }, 185 WINNT: { 186 process: "Utility Generic", 187 decoder: "ffvpx audio decoder", 188 }, 189 Darwin: { 190 process: "Utility Generic", 191 decoder: "ffvpx audio decoder", 192 }, 193 }, 194 }, 195 ]; 196 } 197 198 function audioTestDataEME() { 199 return [ 200 { 201 src: { 202 audioFile: 203 "https://example.com/browser/ipc/glue/test/browser/short-aac-encrypted-audio.mp4", 204 sourceBuffer: "audio/mp4", 205 }, 206 expectations: { 207 Linux: { 208 process: "Utility Generic", 209 decoder: "ffmpeg audio decoder", 210 }, 211 WINNT: { 212 process: "Utility WMF", 213 decoder: "wmf audio decoder", 214 }, 215 Darwin: { 216 process: "Utility AppleMedia", 217 decoder: "apple coremedia decoder", 218 }, 219 }, 220 }, 221 ]; 222 } 223 224 async function addMediaTab(src) { 225 const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", { 226 forceNewProcess: true, 227 }); 228 const browser = gBrowser.getBrowserForTab(tab); 229 await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" }); 230 await SpecialPowers.spawn(browser, [src], createAudioElement); 231 return tab; 232 } 233 234 async function addMediaTabWithEME(sourceBuffer, audioFile) { 235 const tab = BrowserTestUtils.addTab( 236 gBrowser, 237 "https://example.com/browser/", 238 { 239 forceNewProcess: true, 240 } 241 ); 242 const browser = gBrowser.getBrowserForTab(tab); 243 await BrowserTestUtils.browserLoaded(browser); 244 await SpecialPowers.spawn( 245 browser, 246 [sourceBuffer, audioFile], 247 createAudioElementEME 248 ); 249 return tab; 250 } 251 252 async function play( 253 tab, 254 expectUtility, 255 expectDecoder, 256 expectContent = false, 257 expectJava = false, 258 expectError = false, 259 withEME = false 260 ) { 261 let browser = tab.linkedBrowser; 262 return SpecialPowers.spawn( 263 browser, 264 [ 265 expectUtility, 266 expectDecoder, 267 expectContent, 268 expectJava, 269 expectError, 270 withEME, 271 ], 272 checkAudioDecoder 273 ); 274 } 275 276 async function stop(tab) { 277 let browser = tab.linkedBrowser; 278 await SpecialPowers.spawn(browser, [], async function () { 279 let audio = content.document.querySelector("audio"); 280 audio.pause(); 281 }); 282 } 283 284 async function createAudioElement(src) { 285 const doc = typeof content !== "undefined" ? content.document : document; 286 const ROOT = "https://example.com/browser/ipc/glue/test/browser"; 287 let audio = doc.createElement("audio"); 288 audio.setAttribute("controls", "true"); 289 audio.setAttribute("loop", true); 290 audio.src = `${ROOT}/${src}`; 291 doc.body.appendChild(audio); 292 } 293 294 async function createAudioElementEME(sourceBuffer, audioFile) { 295 // Helper to clone data into content so the EME helper can use the data. 296 function cloneIntoContent(data) { 297 return Cu.cloneInto(data, content.wrappedJSObject); 298 } 299 300 // Load the EME helper into content. 301 Services.scriptloader.loadSubScript( 302 "chrome://mochitests/content/browser/ipc/glue/test/browser/eme_standalone.js", 303 content 304 ); 305 306 let audio = content.document.createElement("audio"); 307 audio.setAttribute("controls", "true"); 308 audio.setAttribute("loop", true); 309 audio.setAttribute("_sourceBufferType", sourceBuffer); 310 audio.setAttribute("_audioUrl", audioFile); 311 content.document.body.appendChild(audio); 312 313 let emeHelper = new content.wrappedJSObject.EmeHelper(); 314 emeHelper.SetKeySystem( 315 content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString() 316 ); 317 emeHelper.SetInitDataTypes(cloneIntoContent(["keyids", "cenc"])); 318 emeHelper.SetAudioCapabilities( 319 cloneIntoContent([{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }]) 320 ); 321 emeHelper.AddKeyIdAndKey( 322 "2cdb0ed6119853e7850671c3e9906c3c", 323 "808B9ADAC384DE1E4F56140F4AD76194" 324 ); 325 emeHelper.onerror = error => { 326 is(false, `Got unexpected error from EME helper: ${error}`); 327 }; 328 await emeHelper.ConfigureEme(audio); 329 // Done setting up EME. 330 } 331 332 async function checkAudioDecoder( 333 expectedProcess, 334 expectedDecoder, 335 expectContent = false, 336 expectJava = false, 337 expectError = false, 338 withEME = false 339 ) { 340 const doc = typeof content !== "undefined" ? content.document : document; 341 let audio = doc.querySelector("audio"); 342 const checkPromise = new Promise((resolve, reject) => { 343 const timeUpdateHandler = async () => { 344 const debugInfo = await SpecialPowers.wrap(audio).mozRequestDebugInfo(); 345 const audioDecoderName = debugInfo.decoder.reader.audioDecoderName; 346 347 const isExpectedDecoder = 348 audioDecoderName.indexOf(`${expectedDecoder}`) == 0; 349 ok( 350 isExpectedDecoder, 351 `playback ${audio.src} was from decoder '${audioDecoderName}', expected '${expectedDecoder}'` 352 ); 353 354 const isExpectedProcess = 355 audioDecoderName.indexOf(`(${expectedProcess} remote)`) > 0; 356 const isJavaRemote = audioDecoderName.indexOf("(remote)") > 0; 357 const isOk = 358 (isExpectedProcess && !isJavaRemote && !expectContent && !expectJava) || // Running in Utility 359 (expectJava && !isExpectedProcess && isJavaRemote) || // Running in Java remote 360 (expectContent && !isExpectedProcess && !isJavaRemote); // Running in Content 361 362 ok( 363 isOk, 364 `playback ${audio.src} was from process '${audioDecoderName}', expected '${expectedProcess}'` 365 ); 366 367 if (isOk) { 368 resolve(); 369 } else { 370 reject(); 371 } 372 }; 373 374 const startPlaybackHandler = async () => { 375 ok( 376 await audio.play().then( 377 _ => true, 378 _ => false 379 ), 380 "audio started playing" 381 ); 382 383 audio.addEventListener("timeupdate", timeUpdateHandler, { once: true }); 384 }; 385 386 audio.addEventListener("error", async () => { 387 info( 388 `Received HTML media error: ${audio.error.code}: ${audio.error.message}` 389 ); 390 if (expectError) { 391 const w = typeof content !== "undefined" ? content.window : window; 392 ok( 393 audio.error.code === w.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED || 394 w.MediaError.MEDIA_ERR_DECODE, 395 "Media supported but decoding failed" 396 ); 397 resolve(); 398 } else { 399 info(`Unexpected error`); 400 reject(); 401 } 402 }); 403 404 audio.addEventListener("canplaythrough", startPlaybackHandler, { 405 once: true, 406 }); 407 }); 408 409 if (!withEME) { 410 // We need to make sure the decoder is ready before play()ing otherwise we 411 // could get into bad situations 412 audio.load(); 413 } else { 414 // For EME we need to create and load content ourselves. We do this here 415 // because if we do it in createAudioElementEME() above then we end up 416 // with events fired before we get a chance to listen to them here 417 async function once(target, name) { 418 return new Promise(r => target.addEventListener(name, r, { once: true })); 419 } 420 421 // Setup MSE. 422 const ms = new content.wrappedJSObject.MediaSource(); 423 audio.src = content.wrappedJSObject.URL.createObjectURL(ms); 424 await once(ms, "sourceopen"); 425 const sb = ms.addSourceBuffer(audio.getAttribute("_sourceBufferType")); 426 let fetchResponse = await content.fetch(audio.getAttribute("_audioUrl")); 427 let dataBuffer = await fetchResponse.arrayBuffer(); 428 sb.appendBuffer(dataBuffer); 429 await once(sb, "updateend"); 430 ms.endOfStream(); 431 await once(ms, "sourceended"); 432 } 433 434 return checkPromise; 435 } 436 437 async function runMochitestUtilityAudio( 438 src, 439 { 440 expectUtility, 441 expectDecoder, 442 expectContent = false, 443 expectJava = false, 444 expectError = false, 445 } = {} 446 ) { 447 info(`Add media: ${src}`); 448 await createAudioElement(src); 449 let audio = document.querySelector("audio"); 450 ok(audio, "Found an audio element created"); 451 452 info(`Play media: ${src}`); 453 await checkAudioDecoder( 454 expectUtility, 455 expectDecoder, 456 expectContent, 457 expectJava, 458 expectError 459 ); 460 461 info(`Pause media: ${src}`); 462 await audio.pause(); 463 464 info(`Remove media: ${src}`); 465 document.body.removeChild(audio); 466 } 467 468 async function crashSomeUtility(utilityPid, actorsCheck) { 469 SimpleTest.expectChildProcessCrash(); 470 471 const crashMan = Services.crashmanager; 472 const utilityProcessGone = TestUtils.topicObserved( 473 "ipc:utility-shutdown", 474 (subject, data) => { 475 info(`ipc:utility-shutdown: data=${data} subject=${subject}`); 476 return parseInt(data, 10) === utilityPid; 477 } 478 ); 479 480 info("prune any previous crashes"); 481 const future = new Date(Date.now() + 1000 * 60 * 60 * 24); 482 await crashMan.pruneOldCrashes(future); 483 484 info("crash Utility Process"); 485 const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( 486 Ci.nsIProcessToolsService 487 ); 488 489 info(`Crash Utility Process ${utilityPid}`); 490 ProcessTools.crash(utilityPid); 491 492 info(`Waiting for utility process ${utilityPid} to go away.`); 493 let [subject, data] = await utilityProcessGone; 494 Assert.strictEqual( 495 parseInt(data, 10), 496 utilityPid, 497 `Should match the crashed PID ${utilityPid} with ${data}` 498 ); 499 ok( 500 subject instanceof Ci.nsIPropertyBag2, 501 "Subject needs to be a nsIPropertyBag2 to clean up properly" 502 ); 503 504 // Make sure the process is dead, otherwise there is a risk of race for 505 // writing leak logs 506 utilityProcessTest().noteIntentionalCrash(utilityPid); 507 508 const dumpID = subject.getPropertyAsAString("dumpID"); 509 ok(dumpID, "There should be a dumpID"); 510 511 await crashMan.ensureCrashIsPresent(dumpID); 512 await crashMan.getCrashes().then(crashes => { 513 is(crashes.length, 1, "There should be only one record"); 514 const crash = crashes[0]; 515 ok( 516 crash.isOfType( 517 crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY], 518 crashMan.CRASH_TYPE_CRASH 519 ), 520 "Record should be a utility process crash" 521 ); 522 Assert.strictEqual(crash.id, dumpID, "Record should have an ID"); 523 ok( 524 actorsCheck(crash.metadata.UtilityActorsName), 525 `Record should have the correct actors name for: ${crash.metadata.UtilityActorsName}` 526 ); 527 }); 528 529 let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); 530 minidumpDirectory.append("minidumps"); 531 532 let dumpfile = minidumpDirectory.clone(); 533 dumpfile.append(dumpID + ".dmp"); 534 if (dumpfile.exists()) { 535 info(`Removal of ${dumpfile.path}`); 536 dumpfile.remove(false); 537 } 538 539 let extrafile = minidumpDirectory.clone(); 540 extrafile.append(dumpID + ".extra"); 541 info(`Removal of ${extrafile.path}`); 542 if (extrafile.exists()) { 543 extrafile.remove(false); 544 } 545 } 546 547 // Crash a utility process and generate a crash dump. To close a utility 548 // process (forcefully or not) without a generating a crash, use 549 // cleanUtilityProcessShutdown. 550 async function crashSomeUtilityActor( 551 actor, 552 actorsCheck = () => { 553 return true; 554 } 555 ) { 556 // Get PID for utility type 557 const procInfos = await getUtilityProcesses(actor); 558 Assert.equal( 559 procInfos.length, 560 1, 561 `exactly one ${actor} utility process should be found` 562 ); 563 const utilityPid = procInfos[0].pid; 564 return crashSomeUtility(utilityPid, actorsCheck); 565 } 566 567 function isNightlyOnly() { 568 const { AppConstants } = ChromeUtils.importESModule( 569 "resource://gre/modules/AppConstants.sys.mjs" 570 ); 571 return AppConstants.NIGHTLY_BUILD; 572 }