From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- ipc/glue/test/browser/head.js | 562 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 562 insertions(+) create mode 100644 ipc/glue/test/browser/head.js (limited to 'ipc/glue/test/browser/head.js') diff --git a/ipc/glue/test/browser/head.js b/ipc/glue/test/browser/head.js new file mode 100644 index 0000000000..8acff88273 --- /dev/null +++ b/ipc/glue/test/browser/head.js @@ -0,0 +1,562 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const utilityProcessTest = () => { + return Cc["@mozilla.org/utility-process-test;1"].createInstance( + Ci.nsIUtilityProcessTest + ); +}; + +const kGenericUtilitySandbox = 0; +const kGenericUtilityActor = "unknown"; + +// Start a generic utility process with the given array of utility actor names +// registered. +async function startUtilityProcess(actors = []) { + info("Start a UtilityProcess"); + return utilityProcessTest().startProcess(actors); +} + +// Returns an array of process infos for utility processes of the given type +// or all utility processes if actor is not defined. +async function getUtilityProcesses(actor = undefined, options = {}) { + let procInfos = (await ChromeUtils.requestProcInfo()).children.filter(p => { + return ( + p.type === "utility" && + (actor == undefined || + p.utilityActors.find(a => a.actorName.startsWith(actor))) + ); + }); + + if (!options?.quiet) { + info(`Utility process infos = ${JSON.stringify(procInfos)}`); + } + return procInfos; +} + +async function tryGetUtilityPid(actor, options = {}) { + let process = await getUtilityProcesses(actor, options); + if (!options?.quiet) { + ok(process.length <= 1, `at most one ${actor} process exists`); + } + return process[0]?.pid; +} + +async function checkUtilityExists(actor) { + info(`Looking for a running ${actor} utility process`); + const utilityPid = await tryGetUtilityPid(actor); + ok(utilityPid > 0, `Found ${actor} utility process ${utilityPid}`); + return utilityPid; +} + +// "Cleanly stop" a utility process. This will never leave a crash dump file. +// preferKill will "kill" the process (e.g. SIGABRT) instead of using the +// UtilityProcessManager. +// To "crash" -- i.e. shutdown and generate a crash dump -- use +// crashSomeUtility(). +async function cleanUtilityProcessShutdown(actor, preferKill = false) { + info(`${preferKill ? "Kill" : "Clean shutdown"} Utility Process ${actor}`); + + const utilityPid = await tryGetUtilityPid(actor); + ok(utilityPid !== undefined, `Must have PID for ${actor} utility process`); + + const utilityProcessGone = TestUtils.topicObserved( + "ipc:utility-shutdown", + (subject, data) => parseInt(data, 10) === utilityPid + ); + + if (preferKill) { + SimpleTest.expectChildProcessCrash(); + info(`Kill Utility Process ${utilityPid}`); + const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( + Ci.nsIProcessToolsService + ); + ProcessTools.kill(utilityPid); + } else { + info(`Stopping Utility Process ${utilityPid}`); + await utilityProcessTest().stopProcess(actor); + } + + let [subject, data] = await utilityProcessGone; + ok( + subject instanceof Ci.nsIPropertyBag2, + "Subject needs to be a nsIPropertyBag2 to clean up properly" + ); + is( + parseInt(data, 10), + utilityPid, + `Should match the crashed PID ${utilityPid} with ${data}` + ); + + // Make sure the process is dead, otherwise there is a risk of race for + // writing leak logs + utilityProcessTest().noteIntentionalCrash(utilityPid); + + ok(!subject.hasKey("dumpID"), "There should be no dumpID"); +} + +async function killUtilityProcesses() { + let utilityProcesses = await getUtilityProcesses(); + for (const utilityProcess of utilityProcesses) { + for (const actor of utilityProcess.utilityActors) { + info(`Stopping ${actor.actorName} utility process`); + await cleanUtilityProcessShutdown(actor.actorName, /* preferKill */ true); + } + } +} + +function audioTestData() { + return [ + { + src: "small-shot.ogg", + expectations: { + Android: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + Linux: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + WINNT: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + Darwin: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + }, + }, + { + src: "small-shot.mp3", + expectations: { + Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" }, + Linux: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + WINNT: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + Darwin: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + }, + }, + { + src: "small-shot.m4a", + expectations: { + // Add Android after Bug 1771196 + Linux: { + process: "Utility Generic", + decoder: "ffmpeg audio decoder", + }, + WINNT: { + process: "Utility WMF", + decoder: "wmf audio decoder", + }, + Darwin: { + process: "Utility AppleMedia", + decoder: "apple coremedia decoder", + }, + }, + }, + { + src: "small-shot.flac", + expectations: { + Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" }, + Linux: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + WINNT: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + Darwin: { + process: "Utility Generic", + decoder: "ffvpx audio decoder", + }, + }, + }, + ]; +} + +function audioTestDataEME() { + return [ + { + src: { + audioFile: + "https://example.com/browser/ipc/glue/test/browser/short-aac-encrypted-audio.mp4", + sourceBuffer: "audio/mp4", + }, + expectations: { + Linux: { + process: "Utility Generic", + decoder: "ffmpeg audio decoder", + }, + WINNT: { + process: "Utility WMF", + decoder: "wmf audio decoder", + }, + Darwin: { + process: "Utility AppleMedia", + decoder: "apple coremedia decoder", + }, + }, + }, + ]; +} + +async function addMediaTab(src) { + const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + forceNewProcess: true, + }); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn(browser, [src], createAudioElement); + return tab; +} + +async function addMediaTabWithEME(sourceBuffer, audioFile) { + const tab = BrowserTestUtils.addTab( + gBrowser, + "https://example.com/browser/", + { + forceNewProcess: true, + } + ); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + await SpecialPowers.spawn( + browser, + [sourceBuffer, audioFile], + createAudioElementEME + ); + return tab; +} + +async function play( + tab, + expectUtility, + expectDecoder, + expectContent = false, + expectJava = false, + expectError = false, + withEME = false +) { + let browser = tab.linkedBrowser; + return SpecialPowers.spawn( + browser, + [ + expectUtility, + expectDecoder, + expectContent, + expectJava, + expectError, + withEME, + ], + checkAudioDecoder + ); +} + +async function stop(tab) { + let browser = tab.linkedBrowser; + await SpecialPowers.spawn(browser, [], async function () { + let audio = content.document.querySelector("audio"); + audio.pause(); + }); +} + +async function createAudioElement(src) { + const doc = typeof content !== "undefined" ? content.document : document; + const ROOT = "https://example.com/browser/ipc/glue/test/browser"; + let audio = doc.createElement("audio"); + audio.setAttribute("controls", "true"); + audio.setAttribute("loop", true); + audio.src = `${ROOT}/${src}`; + doc.body.appendChild(audio); +} + +async function createAudioElementEME(sourceBuffer, audioFile) { + // Helper to clone data into content so the EME helper can use the data. + function cloneIntoContent(data) { + return Cu.cloneInto(data, content.wrappedJSObject); + } + + // Load the EME helper into content. + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/ipc/glue/test/browser/eme_standalone.js", + content + ); + + let audio = content.document.createElement("audio"); + audio.setAttribute("controls", "true"); + audio.setAttribute("loop", true); + audio.setAttribute("_sourceBufferType", sourceBuffer); + audio.setAttribute("_audioUrl", audioFile); + content.document.body.appendChild(audio); + + let emeHelper = new content.wrappedJSObject.EmeHelper(); + emeHelper.SetKeySystem( + content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString() + ); + emeHelper.SetInitDataTypes(cloneIntoContent(["keyids", "cenc"])); + emeHelper.SetAudioCapabilities( + cloneIntoContent([{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }]) + ); + emeHelper.AddKeyIdAndKey( + "2cdb0ed6119853e7850671c3e9906c3c", + "808B9ADAC384DE1E4F56140F4AD76194" + ); + emeHelper.onerror = error => { + is(false, `Got unexpected error from EME helper: ${error}`); + }; + await emeHelper.ConfigureEme(audio); + // Done setting up EME. +} + +async function checkAudioDecoder( + expectedProcess, + expectedDecoder, + expectContent = false, + expectJava = false, + expectError = false, + withEME = false +) { + const doc = typeof content !== "undefined" ? content.document : document; + let audio = doc.querySelector("audio"); + const checkPromise = new Promise((resolve, reject) => { + const timeUpdateHandler = async ev => { + const debugInfo = await SpecialPowers.wrap(audio).mozRequestDebugInfo(); + const audioDecoderName = debugInfo.decoder.reader.audioDecoderName; + + const isExpectedDecoder = + audioDecoderName.indexOf(`${expectedDecoder}`) == 0; + ok( + isExpectedDecoder, + `playback ${audio.src} was from decoder '${audioDecoderName}', expected '${expectedDecoder}'` + ); + + const isExpectedProcess = + audioDecoderName.indexOf(`(${expectedProcess} remote)`) > 0; + const isJavaRemote = audioDecoderName.indexOf("(remote)") > 0; + const isOk = + (isExpectedProcess && !isJavaRemote && !expectContent && !expectJava) || // Running in Utility + (expectJava && !isExpectedProcess && isJavaRemote) || // Running in Java remote + (expectContent && !isExpectedProcess && !isJavaRemote); // Running in Content + + ok( + isOk, + `playback ${audio.src} was from process '${audioDecoderName}', expected '${expectedProcess}'` + ); + + if (isOk) { + resolve(); + } else { + reject(); + } + }; + + const startPlaybackHandler = async ev => { + ok( + await audio.play().then( + _ => true, + _ => false + ), + "audio started playing" + ); + + audio.addEventListener("timeupdate", timeUpdateHandler, { once: true }); + }; + + audio.addEventListener("error", async err => { + info( + `Received HTML media error: ${audio.error.code}: ${audio.error.message}` + ); + if (expectError) { + const w = typeof content !== "undefined" ? content.window : window; + ok( + audio.error.code === w.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED || + w.MediaError.MEDIA_ERR_DECODE, + "Media supported but decoding failed" + ); + resolve(); + } else { + info(`Unexpected error`); + reject(); + } + }); + + audio.addEventListener("canplaythrough", startPlaybackHandler, { + once: true, + }); + }); + + if (!withEME) { + // We need to make sure the decoder is ready before play()ing otherwise we + // could get into bad situations + audio.load(); + } else { + // For EME we need to create and load content ourselves. We do this here + // because if we do it in createAudioElementEME() above then we end up + // with events fired before we get a chance to listen to them here + async function once(target, name) { + return new Promise(r => target.addEventListener(name, r, { once: true })); + } + + // Setup MSE. + const ms = new content.wrappedJSObject.MediaSource(); + audio.src = content.wrappedJSObject.URL.createObjectURL(ms); + await once(ms, "sourceopen"); + const sb = ms.addSourceBuffer(audio.getAttribute("_sourceBufferType")); + let fetchResponse = await content.fetch(audio.getAttribute("_audioUrl")); + let dataBuffer = await fetchResponse.arrayBuffer(); + sb.appendBuffer(dataBuffer); + await once(sb, "updateend"); + ms.endOfStream(); + await once(ms, "sourceended"); + } + + return checkPromise; +} + +async function runMochitestUtilityAudio( + src, + { + expectUtility, + expectDecoder, + expectContent = false, + expectJava = false, + expectError = false, + } = {} +) { + info(`Add media: ${src}`); + await createAudioElement(src); + let audio = document.querySelector("audio"); + ok(audio, "Found an audio element created"); + + info(`Play media: ${src}`); + await checkAudioDecoder( + expectUtility, + expectDecoder, + expectContent, + expectJava, + expectError + ); + + info(`Pause media: ${src}`); + await audio.pause(); + + info(`Remove media: ${src}`); + document.body.removeChild(audio); +} + +async function crashSomeUtility(utilityPid, actorsCheck) { + SimpleTest.expectChildProcessCrash(); + + const crashMan = Services.crashmanager; + const utilityProcessGone = TestUtils.topicObserved( + "ipc:utility-shutdown", + (subject, data) => { + info(`ipc:utility-shutdown: data=${data} subject=${subject}`); + return parseInt(data, 10) === utilityPid; + } + ); + + info("prune any previous crashes"); + const future = new Date(Date.now() + 1000 * 60 * 60 * 24); + await crashMan.pruneOldCrashes(future); + + info("crash Utility Process"); + const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService( + Ci.nsIProcessToolsService + ); + + info(`Crash Utility Process ${utilityPid}`); + ProcessTools.crash(utilityPid); + + info(`Waiting for utility process ${utilityPid} to go away.`); + let [subject, data] = await utilityProcessGone; + ok( + parseInt(data, 10) === utilityPid, + `Should match the crashed PID ${utilityPid} with ${data}` + ); + ok( + subject instanceof Ci.nsIPropertyBag2, + "Subject needs to be a nsIPropertyBag2 to clean up properly" + ); + + // Make sure the process is dead, otherwise there is a risk of race for + // writing leak logs + utilityProcessTest().noteIntentionalCrash(utilityPid); + + const dumpID = subject.getPropertyAsAString("dumpID"); + ok(dumpID, "There should be a dumpID"); + + await crashMan.ensureCrashIsPresent(dumpID); + await crashMan.getCrashes().then(crashes => { + is(crashes.length, 1, "There should be only one record"); + const crash = crashes[0]; + ok( + crash.isOfType( + crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY], + crashMan.CRASH_TYPE_CRASH + ), + "Record should be a utility process crash" + ); + ok(crash.id === dumpID, "Record should have an ID"); + ok( + actorsCheck(crash.metadata.UtilityActorsName), + `Record should have the correct actors name for: ${crash.metadata.UtilityActorsName}` + ); + }); + + let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); + minidumpDirectory.append("minidumps"); + + let dumpfile = minidumpDirectory.clone(); + dumpfile.append(dumpID + ".dmp"); + if (dumpfile.exists()) { + info(`Removal of ${dumpfile.path}`); + dumpfile.remove(false); + } + + let extrafile = minidumpDirectory.clone(); + extrafile.append(dumpID + ".extra"); + info(`Removal of ${extrafile.path}`); + if (extrafile.exists()) { + extrafile.remove(false); + } +} + +// Crash a utility process and generate a crash dump. To close a utility +// process (forcefully or not) without a generating a crash, use +// cleanUtilityProcessShutdown. +async function crashSomeUtilityActor( + actor, + actorsCheck = () => { + return true; + } +) { + // Get PID for utility type + const procInfos = await getUtilityProcesses(actor); + ok( + procInfos.length == 1, + `exactly one ${actor} utility process should be found` + ); + const utilityPid = procInfos[0].pid; + return crashSomeUtility(utilityPid, actorsCheck); +} + +function isNightlyOnly() { + const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + return AppConstants.NIGHTLY_BUILD; +} -- cgit v1.2.3