572 lines
16 KiB
JavaScript
572 lines
16 KiB
JavaScript
/* 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) {
|
|
Assert.lessOrEqual(
|
|
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);
|
|
Assert.greater(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);
|
|
Assert.notStrictEqual(
|
|
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 1934009
|
|
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 () => {
|
|
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 () => {
|
|
ok(
|
|
await audio.play().then(
|
|
_ => true,
|
|
_ => false
|
|
),
|
|
"audio started playing"
|
|
);
|
|
|
|
audio.addEventListener("timeupdate", timeUpdateHandler, { once: true });
|
|
};
|
|
|
|
audio.addEventListener("error", async () => {
|
|
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;
|
|
Assert.strictEqual(
|
|
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"
|
|
);
|
|
Assert.strictEqual(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);
|
|
Assert.equal(
|
|
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;
|
|
}
|