Bug 1866777 - Disable test_race_cache_with_network.js on windows opt for frequent...
[gecko.git] / testing / specialpowers / content / SpecialPowersParent.sys.mjs
blob3ed660d71ca35c1d57432f054f798f59983d2184
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   ExtensionData: "resource://gre/modules/Extension.sys.mjs",
11   ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.sys.mjs",
12   HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs",
13   PerTestCoverageUtils:
14     "resource://testing-common/PerTestCoverageUtils.sys.mjs",
15   ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
16   SpecialPowersSandbox:
17     "resource://testing-common/SpecialPowersSandbox.sys.mjs",
18 });
20 class SpecialPowersError extends Error {
21   get name() {
22     return "SpecialPowersError";
23   }
26 const PREF_TYPES = {
27   [Ci.nsIPrefBranch.PREF_INVALID]: "INVALID",
28   [Ci.nsIPrefBranch.PREF_INT]: "INT",
29   [Ci.nsIPrefBranch.PREF_BOOL]: "BOOL",
30   [Ci.nsIPrefBranch.PREF_STRING]: "STRING",
31   number: "INT",
32   boolean: "BOOL",
33   string: "STRING",
36 // We share a single preference environment stack between all
37 // SpecialPowers instances, across all processes.
38 let prefUndoStack = [];
39 let inPrefEnvOp = false;
41 let permissionUndoStack = [];
43 function doPrefEnvOp(fn) {
44   if (inPrefEnvOp) {
45     throw new Error(
46       "Reentrant preference environment operations not supported"
47     );
48   }
49   inPrefEnvOp = true;
50   try {
51     return fn();
52   } finally {
53     inPrefEnvOp = false;
54   }
57 async function createWindowlessBrowser({ isPrivate = false } = {}) {
58   const { promiseDocumentLoaded, promiseEvent, promiseObserved } =
59     ChromeUtils.importESModule(
60       "resource://gre/modules/ExtensionUtils.sys.mjs"
61     ).ExtensionUtils;
63   let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
65   if (isPrivate) {
66     let loadContext = windowlessBrowser.docShell.QueryInterface(
67       Ci.nsILoadContext
68     );
69     loadContext.usePrivateBrowsing = true;
70   }
72   let chromeShell = windowlessBrowser.docShell.QueryInterface(
73     Ci.nsIWebNavigation
74   );
76   const system = Services.scriptSecurityManager.getSystemPrincipal();
77   chromeShell.createAboutBlankDocumentViewer(system, system);
78   windowlessBrowser.browsingContext.useGlobalHistory = false;
79   chromeShell.loadURI(
80     Services.io.newURI("chrome://extensions/content/dummy.xhtml"),
81     {
82       triggeringPrincipal: system,
83     }
84   );
86   await promiseObserved(
87     "chrome-document-global-created",
88     win => win.document == chromeShell.document
89   );
91   let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
93   let browser = chromeDoc.createXULElement("browser");
94   browser.setAttribute("type", "content");
95   browser.setAttribute("disableglobalhistory", "true");
96   browser.setAttribute("remote", "true");
98   let promise = promiseEvent(browser, "XULFrameLoaderCreated");
99   chromeDoc.documentElement.appendChild(browser);
101   await promise;
103   return { windowlessBrowser, browser };
106 // Supplies the unique IDs for tasks created by SpecialPowers.spawn(),
107 // used to bounce assertion messages back down to the correct child.
108 let nextTaskID = 1;
110 // The default actor to send assertions to if a task originated in a
111 // window without a test harness.
112 let defaultAssertHandler;
114 export class SpecialPowersParent extends JSWindowActorParent {
115   constructor() {
116     super();
118     this._messageManager = Services.mm;
119     this._serviceWorkerListener = null;
121     this._observer = this.observe.bind(this);
123     this.didDestroy = this.uninit.bind(this);
125     this._registerObservers = {
126       _self: this,
127       _topics: [],
128       _add(topic) {
129         if (!this._topics.includes(topic)) {
130           this._topics.push(topic);
131           Services.obs.addObserver(this, topic);
132         }
133       },
134       observe(aSubject, aTopic, aData) {
135         var msg = { aData };
136         switch (aTopic) {
137           case "csp-on-violate-policy":
138             // the subject is either an nsIURI or an nsISupportsCString
139             let subject = null;
140             if (aSubject instanceof Ci.nsIURI) {
141               subject = aSubject.asciiSpec;
142             } else if (aSubject instanceof Ci.nsISupportsCString) {
143               subject = aSubject.data;
144             } else {
145               throw new Error("Subject must be nsIURI or nsISupportsCString");
146             }
147             msg = {
148               subject,
149               data: aData,
150             };
151             this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
152             return;
153           case "xfo-on-violate-policy":
154             let uriSpec = null;
155             if (aSubject instanceof Ci.nsIURI) {
156               uriSpec = aSubject.asciiSpec;
157             } else {
158               throw new Error("Subject must be nsIURI");
159             }
160             msg = {
161               subject: uriSpec,
162               data: aData,
163             };
164             this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
165             return;
166           default:
167             this._self.sendAsyncMessage("specialpowers-" + aTopic, msg);
168         }
169       },
170     };
172     this._basePrefs = null;
173     this.init();
175     this._crashDumpDir = null;
176     this._processCrashObserversRegistered = false;
177     this._chromeScriptListeners = [];
178     this._extensions = new Map();
179     this._taskActors = new Map();
180   }
182   static registerActor() {
183     ChromeUtils.registerWindowActor("SpecialPowers", {
184       allFrames: true,
185       includeChrome: true,
186       child: {
187         esModuleURI: "resource://testing-common/SpecialPowersChild.sys.mjs",
188         observers: [
189           "chrome-document-global-created",
190           "content-document-global-created",
191         ],
192       },
193       parent: {
194         esModuleURI: "resource://testing-common/SpecialPowersParent.sys.mjs",
195       },
196     });
197   }
199   static unregisterActor() {
200     ChromeUtils.unregisterWindowActor("SpecialPowers");
201   }
203   init() {
204     Services.obs.addObserver(this._observer, "http-on-modify-request");
206     // We would like to check that tests don't leave service workers around
207     // after they finish, but that information lives in the parent process.
208     // Ideally, we'd be able to tell the child processes whenever service
209     // workers are registered or unregistered so they would know at all times,
210     // but service worker lifetimes are complicated enough to make that
211     // difficult. For the time being, let the child process know when a test
212     // registers a service worker so it can ask, synchronously, at the end if
213     // the service worker had unregister called on it.
214     let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
215       Ci.nsIServiceWorkerManager
216     );
217     let self = this;
218     this._serviceWorkerListener = {
219       onRegister() {
220         self.onRegister();
221       },
223       onUnregister() {
224         // no-op
225       },
226     };
227     swm.addListener(this._serviceWorkerListener);
229     this.getBaselinePrefs();
230   }
232   uninit() {
233     if (defaultAssertHandler === this) {
234       defaultAssertHandler = null;
235     }
237     var obs = Services.obs;
238     obs.removeObserver(this._observer, "http-on-modify-request");
239     this._registerObservers._topics.splice(0).forEach(element => {
240       obs.removeObserver(this._registerObservers, element);
241     });
242     this._removeProcessCrashObservers();
244     let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
245       Ci.nsIServiceWorkerManager
246     );
247     swm.removeListener(this._serviceWorkerListener);
248   }
250   observe(aSubject, aTopic, aData) {
251     function addDumpIDToMessage(propertyName) {
252       try {
253         var id = aSubject.getPropertyAsAString(propertyName);
254       } catch (ex) {
255         id = null;
256       }
257       if (id) {
258         message.dumpIDs.push({ id, extension: "dmp" });
259         message.dumpIDs.push({ id, extension: "extra" });
260       }
261     }
263     switch (aTopic) {
264       case "http-on-modify-request":
265         if (aSubject instanceof Ci.nsIChannel) {
266           let uri = aSubject.URI.spec;
267           this.sendAsyncMessage("specialpowers-http-notify-request", { uri });
268         }
269         break;
271       case "ipc:content-shutdown":
272         aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2);
273         if (!aSubject.hasKey("abnormal")) {
274           return; // This is a normal shutdown, ignore it
275         }
277         var message = { type: "crash-observed", dumpIDs: [] };
278         addDumpIDToMessage("dumpID");
279         this.sendAsyncMessage("SPProcessCrashService", message);
280         break;
281     }
282   }
284   _getCrashDumpDir() {
285     if (!this._crashDumpDir) {
286       this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
287       this._crashDumpDir.append("minidumps");
288     }
289     return this._crashDumpDir;
290   }
292   _getPendingCrashDumpDir() {
293     if (!this._pendingCrashDumpDir) {
294       this._pendingCrashDumpDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
295       this._pendingCrashDumpDir.append("Crash Reports");
296       this._pendingCrashDumpDir.append("pending");
297     }
298     return this._pendingCrashDumpDir;
299   }
301   _deleteCrashDumpFiles(aFilenames) {
302     var crashDumpDir = this._getCrashDumpDir();
303     if (!crashDumpDir.exists()) {
304       return false;
305     }
307     var success = !!aFilenames.length;
308     aFilenames.forEach(function (crashFilename) {
309       var file = crashDumpDir.clone();
310       file.append(crashFilename);
311       if (file.exists()) {
312         file.remove(false);
313       } else {
314         success = false;
315       }
316     });
317     return success;
318   }
320   _findCrashDumpFiles(aToIgnore) {
321     var crashDumpDir = this._getCrashDumpDir();
322     var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries;
323     if (!entries) {
324       return [];
325     }
327     var crashDumpFiles = [];
328     while (entries.hasMoreElements()) {
329       var file = entries.nextFile;
330       var path = String(file.path);
331       if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) {
332         crashDumpFiles.push(path);
333       }
334     }
335     return crashDumpFiles.concat();
336   }
338   _deletePendingCrashDumpFiles() {
339     var crashDumpDir = this._getPendingCrashDumpDir();
340     var removed = false;
341     if (crashDumpDir.exists()) {
342       let entries = crashDumpDir.directoryEntries;
343       while (entries.hasMoreElements()) {
344         let file = entries.nextFile;
345         if (file.isFile()) {
346           file.remove(false);
347           removed = true;
348         }
349       }
350     }
351     return removed;
352   }
354   _addProcessCrashObservers() {
355     if (this._processCrashObserversRegistered) {
356       return;
357     }
359     Services.obs.addObserver(this._observer, "ipc:content-shutdown");
360     this._processCrashObserversRegistered = true;
361   }
363   _removeProcessCrashObservers() {
364     if (!this._processCrashObserversRegistered) {
365       return;
366     }
368     Services.obs.removeObserver(this._observer, "ipc:content-shutdown");
369     this._processCrashObserversRegistered = false;
370   }
372   onRegister() {
373     this.sendAsyncMessage("SPServiceWorkerRegistered", { registered: true });
374   }
376   _getURI(url) {
377     return Services.io.newURI(url);
378   }
379   _notifyCategoryAndObservers(subject, topic, data) {
380     const serviceMarker = "service,";
382     // First create observers from the category manager.
384     let observers = [];
386     for (let { value: contractID } of Services.catMan.enumerateCategory(
387       topic
388     )) {
389       let factoryFunction;
390       if (contractID.substring(0, serviceMarker.length) == serviceMarker) {
391         contractID = contractID.substring(serviceMarker.length);
392         factoryFunction = "getService";
393       } else {
394         factoryFunction = "createInstance";
395       }
397       try {
398         let handler = Cc[contractID][factoryFunction]();
399         if (handler) {
400           let observer = handler.QueryInterface(Ci.nsIObserver);
401           observers.push(observer);
402         }
403       } catch (e) {}
404     }
406     // Next enumerate the registered observers.
407     for (let observer of Services.obs.enumerateObservers(topic)) {
408       if (observer instanceof Ci.nsIObserver && !observers.includes(observer)) {
409         observers.push(observer);
410       }
411     }
413     observers.forEach(function (observer) {
414       try {
415         observer.observe(subject, topic, data);
416       } catch (e) {}
417     });
418   }
420   /*
421     Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
422     All actions performed must modify the relevant pref.
424     Returns whether we need to wait for a refresh driver tick for the pref to
425     have effect. This is only needed for ui. and font. prefs, which affect the
426     look and feel code and have some change-coalescing going on.
427   */
428   _applyPrefs(actions) {
429     let requiresRefresh = false;
430     for (let pref of actions) {
431       // This logic should match PrefRequiresRefresh in reftest.sys.mjs
432       requiresRefresh =
433         requiresRefresh ||
434         pref.name == "layout.css.prefers-color-scheme.content-override" ||
435         pref.name.startsWith("ui.") ||
436         pref.name.startsWith("browser.display.") ||
437         pref.name.startsWith("font.");
438       if (pref.action == "set") {
439         this._setPref(pref.name, pref.type, pref.value, pref.iid);
440       } else if (pref.action == "clear") {
441         Services.prefs.clearUserPref(pref.name);
442       }
443     }
444     return requiresRefresh;
445   }
447   /**
448    * Take in a list of pref changes to make, pushes their current values
449    * onto the restore stack, and makes the changes.  When the test
450    * finishes, these changes are reverted.
451    *
452    * |inPrefs| must be an object with up to two properties: "set" and "clear".
453    * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
454    * the prefs indicated in |inPrefs.clear|.
455    *
456    * For example, you might pass |inPrefs| as:
457    *
458    *  inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
459    *             'clear': [['clear.this'], ['also.this']] };
460    *
461    * Notice that |set| and |clear| are both an array of arrays.  In |set|, each
462    * of the inner arrays must have the form [pref_name, value] or [pref_name,
463    * value, iid].  (The latter form is used for prefs with "complex" values.)
464    *
465    * In |clear|, each inner array should have the form [pref_name].
466    *
467    * If you set the same pref more than once (or both set and clear a pref),
468    * the behavior of this method is undefined.
469    */
470   pushPrefEnv(inPrefs) {
471     return doPrefEnvOp(() => {
472       let pendingActions = [];
473       let cleanupActions = [];
475       for (let [action, prefs] of Object.entries(inPrefs)) {
476         for (let pref of prefs) {
477           let name = pref[0];
478           let value = null;
479           let iid = null;
480           let type = PREF_TYPES[Services.prefs.getPrefType(name)];
481           let originalValue = null;
483           if (pref.length == 3) {
484             value = pref[1];
485             iid = pref[2];
486           } else if (pref.length == 2) {
487             value = pref[1];
488           }
490           /* If pref is not found or invalid it doesn't exist. */
491           if (type !== "INVALID") {
492             if (
493               (Services.prefs.prefHasUserValue(name) && action == "clear") ||
494               action == "set"
495             ) {
496               originalValue = this._getPref(name, type);
497             }
498           } else if (action == "set") {
499             /* name doesn't exist, so 'clear' is pointless */
500             if (iid) {
501               type = "COMPLEX";
502             }
503           }
505           if (type === "INVALID") {
506             type = PREF_TYPES[typeof value];
507           }
508           if (type === "INVALID") {
509             throw new Error("Unexpected preference type for " + name);
510           }
512           pendingActions.push({ action, type, name, value, iid });
514           /* Push original preference value or clear into cleanup array */
515           var cleanupTodo = { type, name, value: originalValue, iid };
516           if (originalValue == null) {
517             cleanupTodo.action = "clear";
518           } else {
519             cleanupTodo.action = "set";
520           }
521           cleanupActions.push(cleanupTodo);
522         }
523       }
525       prefUndoStack.push(cleanupActions);
526       let requiresRefresh = this._applyPrefs(pendingActions);
527       return { requiresRefresh };
528     });
529   }
531   async popPrefEnv() {
532     return doPrefEnvOp(() => {
533       let env = prefUndoStack.pop();
534       if (env) {
535         let requiresRefresh = this._applyPrefs(env);
536         return { popped: true, requiresRefresh };
537       }
538       return { popped: false, requiresRefresh: false };
539     });
540   }
542   flushPrefEnv() {
543     let requiresRefresh = false;
544     while (prefUndoStack.length) {
545       requiresRefresh |= this.popPrefEnv().requiresRefresh;
546     }
547     return { requiresRefresh };
548   }
550   _setPref(name, type, value, iid) {
551     switch (type) {
552       case "BOOL":
553         return Services.prefs.setBoolPref(name, value);
554       case "INT":
555         return Services.prefs.setIntPref(name, value);
556       case "CHAR":
557         return Services.prefs.setCharPref(name, value);
558       case "COMPLEX":
559         return Services.prefs.setComplexValue(name, iid, value);
560       case "STRING":
561         return Services.prefs.setStringPref(name, value);
562     }
563     switch (typeof value) {
564       case "boolean":
565         return Services.prefs.setBoolPref(name, value);
566       case "number":
567         return Services.prefs.setIntPref(name, value);
568       case "string":
569         return Services.prefs.setStringPref(name, value);
570     }
571     throw new Error(
572       `Unexpected preference type: ${type} for ${name} with value ${value} and type ${typeof value}`
573     );
574   }
576   _getPref(name, type, defaultValue, iid) {
577     switch (type) {
578       case "BOOL":
579         if (defaultValue !== undefined) {
580           return Services.prefs.getBoolPref(name, defaultValue);
581         }
582         return Services.prefs.getBoolPref(name);
583       case "INT":
584         if (defaultValue !== undefined) {
585           return Services.prefs.getIntPref(name, defaultValue);
586         }
587         return Services.prefs.getIntPref(name);
588       case "CHAR":
589         if (defaultValue !== undefined) {
590           return Services.prefs.getCharPref(name, defaultValue);
591         }
592         return Services.prefs.getCharPref(name);
593       case "COMPLEX":
594         return Services.prefs.getComplexValue(name, iid);
595       case "STRING":
596         if (defaultValue !== undefined) {
597           return Services.prefs.getStringPref(name, defaultValue);
598         }
599         return Services.prefs.getStringPref(name);
600     }
601     throw new Error(
602       `Unexpected preference type: ${type} for preference ${name}`
603     );
604   }
606   getBaselinePrefs() {
607     this._basePrefs = this._getAllPreferences();
608   }
610   _comparePrefs(base, target, ignorePrefs, partialMatches) {
611     let failures = [];
612     for (const [key, value] of base) {
613       if (ignorePrefs.includes(key)) {
614         continue;
615       }
616       let partialFind = false;
617       partialMatches.forEach(pm => {
618         if (key.startsWith(pm)) {
619           partialFind = true;
620         }
621       });
622       if (partialFind) {
623         continue;
624       }
626       if (value === target.get(key)) {
627         continue;
628       }
629       if (!failures.includes(key)) {
630         failures.push(key);
631       }
632     }
633     return failures;
634   }
636   comparePrefsToBaseline(ignorePrefs) {
637     let newPrefs = this._getAllPreferences();
639     // find all items in ignorePrefs that end in *, add to partialMatch
640     let partialMatch = [];
641     if (ignorePrefs === undefined) {
642       ignorePrefs = [];
643     }
644     ignorePrefs.forEach(pref => {
645       if (pref.endsWith("*")) {
646         partialMatch.push(pref.split("*")[0]);
647       }
648     });
650     // find all new prefs different than old
651     let rv1 = this._comparePrefs(
652       newPrefs,
653       this._basePrefs,
654       ignorePrefs,
655       partialMatch
656     );
658     // find all old prefs different than new (in case we delete)
659     let rv2 = this._comparePrefs(
660       this._basePrefs,
661       newPrefs,
662       ignorePrefs,
663       partialMatch
664     );
666     let failures = [...new Set([...rv1, ...rv2])];
668     // reset failures
669     failures.forEach(f => {
670       if (this._basePrefs.get(f)) {
671         this._setPref(
672           f,
673           PREF_TYPES[Services.prefs.getPrefType(f)],
674           this._basePrefs.get(f)
675         );
676       } else {
677         Services.prefs.clearUserPref(f);
678       }
679     });
681     if (ignorePrefs.length > 1) {
682       return failures;
683     }
684     return [];
685   }
687   _getAllPreferences() {
688     let names = new Map();
689     for (let prefName of Services.prefs.getChildList("")) {
690       let prefType = PREF_TYPES[Services.prefs.getPrefType(prefName)];
691       let prefValue = this._getPref(prefName, prefType);
692       names.set(prefName, prefValue);
693     }
694     return names;
695   }
697   _toggleMuteAudio(aMuted) {
698     let browser = this.browsingContext.top.embedderElement;
699     if (aMuted) {
700       browser.mute();
701     } else {
702       browser.unmute();
703     }
704   }
706   _permOp(perm) {
707     switch (perm.op) {
708       case "add":
709         Services.perms.addFromPrincipal(
710           perm.principal,
711           perm.type,
712           perm.permission,
713           perm.expireType,
714           perm.expireTime
715         );
716         break;
717       case "remove":
718         Services.perms.removeFromPrincipal(perm.principal, perm.type);
719         break;
720       default:
721         throw new Error(`Unexpected permission op: ${perm.op}`);
722     }
723   }
725   pushPermissions(inPermissions) {
726     let pendingPermissions = [];
727     let cleanupPermissions = [];
729     for (let permission of inPermissions) {
730       let { principal } = permission;
731       if (principal.isSystemPrincipal) {
732         continue;
733       }
735       let originalValue = Services.perms.testPermissionFromPrincipal(
736         principal,
737         permission.type
738       );
740       let perm = permission.allow;
741       if (typeof perm === "boolean") {
742         perm = Ci.nsIPermissionManager[perm ? "ALLOW_ACTION" : "DENY_ACTION"];
743       }
745       if (permission.remove) {
746         perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
747       }
749       if (originalValue == perm) {
750         continue;
751       }
753       let todo = {
754         op: "add",
755         type: permission.type,
756         permission: perm,
757         value: perm,
758         principal,
759         expireType:
760           typeof permission.expireType === "number" ? permission.expireType : 0, // default: EXPIRE_NEVER
761         expireTime:
762           typeof permission.expireTime === "number" ? permission.expireTime : 0,
763       };
765       var cleanupTodo = Object.assign({}, todo);
767       if (permission.remove) {
768         todo.op = "remove";
769       }
771       pendingPermissions.push(todo);
773       if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
774         cleanupTodo.op = "remove";
775       } else {
776         cleanupTodo.value = originalValue;
777         cleanupTodo.permission = originalValue;
778       }
779       cleanupPermissions.push(cleanupTodo);
780     }
782     permissionUndoStack.push(cleanupPermissions);
784     for (let perm of pendingPermissions) {
785       this._permOp(perm);
786     }
787   }
789   popPermissions() {
790     if (permissionUndoStack.length) {
791       for (let perm of permissionUndoStack.pop()) {
792         this._permOp(perm);
793       }
794     }
795   }
797   flushPermissions() {
798     while (permissionUndoStack.length) {
799       this.popPermissions();
800     }
801   }
803   _spawnChrome(task, args, caller, imports) {
804     let sb = new lazy.SpecialPowersSandbox(
805       null,
806       data => {
807         this.sendAsyncMessage("Assert", data);
808       },
809       { imports }
810     );
812     for (let [global, prop] of Object.entries({
813       windowGlobalParent: "manager",
814       browsingContext: "browsingContext",
815     })) {
816       Object.defineProperty(sb.sandbox, global, {
817         get: () => {
818           return this[prop];
819         },
820         enumerable: true,
821       });
822     }
824     return sb.execute(task, args, caller);
825   }
827   /**
828    * messageManager callback function
829    * This will get requests from our API in the window and process them in chrome for it
830    **/
831   // eslint-disable-next-line complexity
832   async receiveMessage(aMessage) {
833     let startTime = Cu.now();
834     // Try block so we can use a finally statement to add a profiler marker
835     // despite all the return statements.
836     try {
837       // We explicitly return values in the below code so that this function
838       // doesn't trigger a flurry of warnings about "does not always return
839       // a value".
840       switch (aMessage.name) {
841         case "SPToggleMuteAudio":
842           return this._toggleMuteAudio(aMessage.data.mute);
844         case "Ping":
845           return undefined;
847         case "SpecialPowers.Quit":
848           if (
849             !AppConstants.RELEASE_OR_BETA &&
850             !AppConstants.DEBUG &&
851             !AppConstants.MOZ_CODE_COVERAGE &&
852             !AppConstants.ASAN &&
853             !AppConstants.TSAN
854           ) {
855             if (Services.profiler.IsActive()) {
856               let filename = Services.env.get("MOZ_PROFILER_SHUTDOWN");
857               if (filename) {
858                 await Services.profiler.dumpProfileToFileAsync(filename);
859                 await Services.profiler.StopProfiler();
860               }
861             }
862             Cu.exitIfInAutomation();
863           } else {
864             Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
865           }
866           return undefined;
868         case "EnsureFocus":
869           let bc = aMessage.data.browsingContext;
870           // Send a message to the child telling it to focus the window.
871           // If the message responds with a browsing context, then
872           // a child browsing context in a subframe should be focused.
873           // Iterate until nothing is returned and we get to the most
874           // deeply nested subframe that should be focused.
875           do {
876             let spParent = bc.currentWindowGlobal.getActor("SpecialPowers");
877             if (spParent) {
878               bc = await spParent.sendQuery("EnsureFocus", {
879                 blurSubframe: aMessage.data.blurSubframe,
880               });
881             }
882           } while (bc && !aMessage.data.blurSubframe);
883           return undefined;
885         case "SpecialPowers.Focus":
886           if (this.manager.rootFrameLoader) {
887             this.manager.rootFrameLoader.ownerElement.focus();
888           }
889           return undefined;
891         case "SpecialPowers.CreateFiles":
892           return (async () => {
893             let filePaths = [];
894             if (!this._createdFiles) {
895               this._createdFiles = [];
896             }
897             let createdFiles = this._createdFiles;
899             let promises = [];
900             aMessage.data.forEach(function (request) {
901               const filePerms = 0o666;
902               let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
903               if (request.name) {
904                 testFile.appendRelativePath(request.name);
905               } else {
906                 testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms);
907               }
908               let outStream = Cc[
909                 "@mozilla.org/network/file-output-stream;1"
910               ].createInstance(Ci.nsIFileOutputStream);
911               outStream.init(
912                 testFile,
913                 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE
914                 filePerms,
915                 0
916               );
917               if (request.data) {
918                 outStream.write(request.data, request.data.length);
919               }
920               outStream.close();
921               promises.push(
922                 File.createFromFileName(testFile.path, request.options).then(
923                   function (file) {
924                     filePaths.push(file);
925                   }
926                 )
927               );
928               createdFiles.push(testFile);
929             });
931             await Promise.all(promises);
932             return filePaths;
933           })().catch(e => {
934             console.error(e);
935             return Promise.reject(String(e));
936           });
938         case "SpecialPowers.RemoveFiles":
939           if (this._createdFiles) {
940             this._createdFiles.forEach(function (testFile) {
941               try {
942                 testFile.remove(false);
943               } catch (e) {}
944             });
945             this._createdFiles = null;
946           }
947           return undefined;
949         case "Wakeup":
950           return undefined;
952         case "EvictAllDocumentViewers":
953           this.browsingContext.top.sessionHistory.evictAllDocumentViewers();
954           return undefined;
956         case "getBaselinePrefs":
957           return this.getBaselinePrefs();
959         case "comparePrefsToBaseline":
960           return this.comparePrefsToBaseline(aMessage.data);
962         case "PushPrefEnv":
963           return this.pushPrefEnv(aMessage.data);
965         case "PopPrefEnv":
966           return this.popPrefEnv();
968         case "FlushPrefEnv":
969           return this.flushPrefEnv();
971         case "PushPermissions":
972           return this.pushPermissions(aMessage.data);
974         case "PopPermissions":
975           return this.popPermissions();
977         case "FlushPermissions":
978           return this.flushPermissions();
980         case "SPPrefService": {
981           let prefs = Services.prefs;
982           let prefType = aMessage.json.prefType.toUpperCase();
983           let { prefName, prefValue, iid, defaultValue } = aMessage.json;
985           if (aMessage.json.op == "get") {
986             if (!prefName || !prefType) {
987               throw new SpecialPowersError(
988                 "Invalid parameters for get in SPPrefService"
989               );
990             }
992             // return null if the pref doesn't exist
993             if (
994               defaultValue === undefined &&
995               prefs.getPrefType(prefName) == prefs.PREF_INVALID
996             ) {
997               return null;
998             }
999             return this._getPref(prefName, prefType, defaultValue, iid);
1000           } else if (aMessage.json.op == "set") {
1001             if (!prefName || !prefType || prefValue === undefined) {
1002               throw new SpecialPowersError(
1003                 "Invalid parameters for set in SPPrefService"
1004               );
1005             }
1007             return this._setPref(prefName, prefType, prefValue, iid);
1008           } else if (aMessage.json.op == "clear") {
1009             if (!prefName) {
1010               throw new SpecialPowersError(
1011                 "Invalid parameters for clear in SPPrefService"
1012               );
1013             }
1015             prefs.clearUserPref(prefName);
1016           } else {
1017             throw new SpecialPowersError("Invalid operation for SPPrefService");
1018           }
1020           return undefined; // See comment at the beginning of this function.
1021         }
1023         case "SPProcessCrashService": {
1024           switch (aMessage.json.op) {
1025             case "register-observer":
1026               this._addProcessCrashObservers();
1027               break;
1028             case "unregister-observer":
1029               this._removeProcessCrashObservers();
1030               break;
1031             case "delete-crash-dump-files":
1032               return this._deleteCrashDumpFiles(aMessage.json.filenames);
1033             case "find-crash-dump-files":
1034               return this._findCrashDumpFiles(
1035                 aMessage.json.crashDumpFilesToIgnore
1036               );
1037             case "delete-pending-crash-dump-files":
1038               return this._deletePendingCrashDumpFiles();
1039             default:
1040               throw new SpecialPowersError(
1041                 "Invalid operation for SPProcessCrashService"
1042               );
1043           }
1044           return undefined; // See comment at the beginning of this function.
1045         }
1047         case "SPProcessCrashManagerWait": {
1048           let promises = aMessage.json.crashIds.map(crashId => {
1049             return Services.crashmanager.ensureCrashIsPresent(crashId);
1050           });
1051           return Promise.all(promises);
1052         }
1054         case "SPPermissionManager": {
1055           let msg = aMessage.data;
1056           switch (msg.op) {
1057             case "add":
1058             case "remove":
1059               this._permOp(msg);
1060               break;
1061             case "has":
1062               let hasPerm = Services.perms.testPermissionFromPrincipal(
1063                 msg.principal,
1064                 msg.type
1065               );
1066               return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
1067             case "test":
1068               let testPerm = Services.perms.testPermissionFromPrincipal(
1069                 msg.principal,
1070                 msg.type
1071               );
1072               return testPerm == msg.value;
1073             default:
1074               throw new SpecialPowersError(
1075                 "Invalid operation for SPPermissionManager"
1076               );
1077           }
1078           return undefined; // See comment at the beginning of this function.
1079         }
1081         case "SPObserverService": {
1082           let topic = aMessage.json.observerTopic;
1083           switch (aMessage.json.op) {
1084             case "notify":
1085               let data = aMessage.json.observerData;
1086               Services.obs.notifyObservers(null, topic, data);
1087               break;
1088             case "add":
1089               this._registerObservers._add(topic);
1090               break;
1091             default:
1092               throw new SpecialPowersError(
1093                 "Invalid operation for SPObserverervice"
1094               );
1095           }
1096           return undefined; // See comment at the beginning of this function.
1097         }
1099         case "SPLoadChromeScript": {
1100           let id = aMessage.json.id;
1101           let scriptName;
1103           let jsScript = aMessage.json.function.body;
1104           if (aMessage.json.url) {
1105             scriptName = aMessage.json.url;
1106           } else if (aMessage.json.function) {
1107             scriptName =
1108               aMessage.json.function.name ||
1109               "<loadChromeScript anonymous function>";
1110           } else {
1111             throw new SpecialPowersError("SPLoadChromeScript: Invalid script");
1112           }
1114           // Setup a chrome sandbox that has access to sendAsyncMessage
1115           // and {add,remove}MessageListener in order to communicate with
1116           // the mochitest.
1117           let sb = new lazy.SpecialPowersSandbox(
1118             scriptName,
1119             data => {
1120               this.sendAsyncMessage("Assert", data);
1121             },
1122             aMessage.data
1123           );
1125           Object.assign(sb.sandbox, {
1126             createWindowlessBrowser,
1127             sendAsyncMessage: (name, message) => {
1128               this.sendAsyncMessage("SPChromeScriptMessage", {
1129                 id,
1130                 name,
1131                 message,
1132               });
1133             },
1134             addMessageListener: (name, listener) => {
1135               this._chromeScriptListeners.push({ id, name, listener });
1136             },
1137             removeMessageListener: (name, listener) => {
1138               let index = this._chromeScriptListeners.findIndex(function (obj) {
1139                 return (
1140                   obj.id == id && obj.name == name && obj.listener == listener
1141                 );
1142               });
1143               if (index >= 0) {
1144                 this._chromeScriptListeners.splice(index, 1);
1145               }
1146             },
1147             actorParent: this.manager,
1148           });
1150           // Evaluate the chrome script
1151           try {
1152             Cu.evalInSandbox(jsScript, sb.sandbox, "1.8", scriptName, 1);
1153           } catch (e) {
1154             throw new SpecialPowersError(
1155               "Error while executing chrome script '" +
1156                 scriptName +
1157                 "':\n" +
1158                 e +
1159                 "\n" +
1160                 e.fileName +
1161                 ":" +
1162                 e.lineNumber
1163             );
1164           }
1165           return undefined; // See comment at the beginning of this function.
1166         }
1168         case "SPChromeScriptMessage": {
1169           let id = aMessage.json.id;
1170           let name = aMessage.json.name;
1171           let message = aMessage.json.message;
1172           let result;
1173           for (let listener of this._chromeScriptListeners) {
1174             if (listener.name == name && listener.id == id) {
1175               result = listener.listener(message);
1176             }
1177           }
1178           return result;
1179         }
1181         case "SPCleanUpSTSData": {
1182           let origin = aMessage.data.origin;
1183           let uri = Services.io.newURI(origin);
1184           let sss = Cc["@mozilla.org/ssservice;1"].getService(
1185             Ci.nsISiteSecurityService
1186           );
1187           sss.resetState(uri);
1188           return undefined;
1189         }
1191         case "SPRequestDumpCoverageCounters": {
1192           return lazy.PerTestCoverageUtils.afterTest();
1193         }
1195         case "SPRequestResetCoverageCounters": {
1196           return lazy.PerTestCoverageUtils.beforeTest();
1197         }
1199         case "SPCheckServiceWorkers": {
1200           let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
1201             Ci.nsIServiceWorkerManager
1202           );
1203           let regs = swm.getAllRegistrations();
1205           // XXX This code is shared with specialpowers.js.
1206           let workers = new Array(regs.length);
1207           for (let i = 0; i < regs.length; ++i) {
1208             let { scope, scriptSpec } = regs.queryElementAt(
1209               i,
1210               Ci.nsIServiceWorkerRegistrationInfo
1211             );
1212             workers[i] = { scope, scriptSpec };
1213           }
1214           return { workers };
1215         }
1217         case "SPLoadExtension": {
1218           let id = aMessage.data.id;
1219           let ext = aMessage.data.ext;
1220           if (AppConstants.platform === "android") {
1221             // Some extension APIs are partially implemented in Java, and the
1222             // interface between the JS and Java side (GeckoViewWebExtension)
1223             // expects extensions to be registered with the AddonManager.
1224             //
1225             // For simplicity, default to using an Addon Manager (if not null).
1226             if (ext.useAddonManager === undefined) {
1227               ext.useAddonManager = "android-only";
1228             }
1229           }
1230           // delayedStartup is only supported in xpcshell
1231           if (ext.delayedStartup !== undefined) {
1232             throw new Error(
1233               `delayedStartup is only supported in xpcshell, use "useAddonManager".`
1234             );
1235           }
1237           let extension = lazy.ExtensionTestCommon.generate(ext);
1239           let resultListener = (...args) => {
1240             this.sendAsyncMessage("SPExtensionMessage", {
1241               id,
1242               type: "testResult",
1243               args,
1244             });
1245           };
1247           let messageListener = (...args) => {
1248             args.shift();
1249             this.sendAsyncMessage("SPExtensionMessage", {
1250               id,
1251               type: "testMessage",
1252               args,
1253             });
1254           };
1256           // Register pass/fail handlers.
1257           extension.on("test-result", resultListener);
1258           extension.on("test-eq", resultListener);
1259           extension.on("test-log", resultListener);
1260           extension.on("test-done", resultListener);
1262           extension.on("test-message", messageListener);
1264           this._extensions.set(id, extension);
1265           return undefined;
1266         }
1268         case "SPStartupExtension": {
1269           let id = aMessage.data.id;
1270           // This is either an Extension, or (if useAddonManager is set) a MockExtension.
1271           let extension = this._extensions.get(id);
1272           extension.on("startup", (eventName, ext) => {
1273             if (AppConstants.platform === "android") {
1274               // We need a way to notify the embedding layer that a new extension
1275               // has been installed, so that the java layer can be updated too.
1276               Services.obs.notifyObservers(null, "testing-installed-addon", id);
1277             }
1278             // ext is always the "real" Extension object, even when "extension"
1279             // is a MockExtension.
1280             this.sendAsyncMessage("SPExtensionMessage", {
1281               id,
1282               type: "extensionSetId",
1283               args: [ext.id, ext.uuid],
1284             });
1285           });
1287           // Make sure the extension passes the packaging checks when
1288           // they're run on a bare archive rather than a running instance,
1289           // as the add-on manager runs them.
1290           let extensionData = new lazy.ExtensionData(extension.rootURI);
1291           return extensionData
1292             .loadManifest()
1293             .then(
1294               () => {
1295                 return extensionData.initAllLocales().then(() => {
1296                   if (extensionData.errors.length) {
1297                     return Promise.reject(
1298                       "Extension contains packaging errors"
1299                     );
1300                   }
1301                   return undefined;
1302                 });
1303               },
1304               () => {
1305                 // loadManifest() will throw if we're loading an embedded
1306                 // extension, so don't worry about locale errors in that
1307                 // case.
1308               }
1309             )
1310             .then(async () => {
1311               // browser tests do not call startup in ExtensionXPCShellUtils or MockExtension,
1312               // in that case we have an ID here and we need to set the override.
1313               if (extension.id) {
1314                 await lazy.ExtensionTestCommon.setIncognitoOverride(extension);
1315               }
1316               return extension.startup().then(
1317                 () => {},
1318                 e => {
1319                   dump(`Extension startup failed: ${e}\n${e.stack}`);
1320                   throw e;
1321                 }
1322               );
1323             });
1324         }
1326         case "SPExtensionMessage": {
1327           let id = aMessage.data.id;
1328           let extension = this._extensions.get(id);
1329           extension.testMessage(...aMessage.data.args);
1330           return undefined;
1331         }
1333         case "SPExtensionGrantActiveTab": {
1334           let { id, tabId } = aMessage.data;
1335           let { tabManager } = this._extensions.get(id);
1336           tabManager.addActiveTabPermission(tabManager.get(tabId).nativeTab);
1337           return undefined;
1338         }
1340         case "SPUnloadExtension": {
1341           let id = aMessage.data.id;
1342           let extension = this._extensions.get(id);
1343           this._extensions.delete(id);
1344           return extension.shutdown().then(() => {
1345             return extension._uninstallPromise;
1346           });
1347         }
1349         case "SPExtensionTerminateBackground": {
1350           let id = aMessage.data.id;
1351           let args = aMessage.data.args;
1352           let extension = this._extensions.get(id);
1353           return extension.terminateBackground(...args);
1354         }
1356         case "SPExtensionWakeupBackground": {
1357           let id = aMessage.data.id;
1358           let extension = this._extensions.get(id);
1359           return extension.wakeupBackground();
1360         }
1362         case "SetAsDefaultAssertHandler": {
1363           defaultAssertHandler = this;
1364           return undefined;
1365         }
1367         case "Spawn": {
1368           // Use a different variable for the profiler marker start time
1369           // so that a marker isn't added when we return, but instead when
1370           // our promise resolves.
1371           let spawnStartTime = startTime;
1372           startTime = undefined;
1373           let { browsingContext, task, args, caller, hasHarness, imports } =
1374             aMessage.data;
1376           let spParent =
1377             browsingContext.currentWindowGlobal.getActor("SpecialPowers");
1379           let taskId = nextTaskID++;
1380           if (hasHarness) {
1381             spParent._taskActors.set(taskId, this);
1382           }
1384           return spParent
1385             .sendQuery("Spawn", { task, args, caller, taskId, imports })
1386             .finally(() => {
1387               ChromeUtils.addProfilerMarker(
1388                 "SpecialPowers",
1389                 { startTime: spawnStartTime, category: "Test" },
1390                 aMessage.name
1391               );
1392               return spParent._taskActors.delete(taskId);
1393             });
1394         }
1396         case "SpawnChrome": {
1397           let { task, args, caller, imports } = aMessage.data;
1399           return this._spawnChrome(task, args, caller, imports);
1400         }
1402         case "Snapshot": {
1403           let { browsingContext, rect, background, resetScrollPosition } =
1404             aMessage.data;
1406           return browsingContext.currentWindowGlobal
1407             .drawSnapshot(rect, 1.0, background, resetScrollPosition)
1408             .then(async image => {
1409               let hiddenFrame = new lazy.HiddenFrame();
1410               let win = await hiddenFrame.get();
1412               let canvas = win.document.createElement("canvas");
1413               canvas.width = image.width;
1414               canvas.height = image.height;
1416               const ctx = canvas.getContext("2d");
1417               ctx.drawImage(image, 0, 0);
1419               let data = ctx.getImageData(0, 0, image.width, image.height);
1420               hiddenFrame.destroy();
1421               return data;
1422             });
1423         }
1425         case "SecurityState": {
1426           let { browsingContext } = aMessage.data;
1427           return browsingContext.secureBrowserUI.state;
1428         }
1430         case "ProxiedAssert": {
1431           let { taskId, data } = aMessage.data;
1433           let actor = this._taskActors.get(taskId) || defaultAssertHandler;
1434           actor.sendAsyncMessage("Assert", data);
1436           return undefined;
1437         }
1439         case "SPRemoveAllServiceWorkers": {
1440           return lazy.ServiceWorkerCleanUp.removeAll();
1441         }
1443         case "SPRemoveServiceWorkerDataForExampleDomain": {
1444           return lazy.ServiceWorkerCleanUp.removeFromHost("example.com");
1445         }
1447         case "SPGenerateMediaControlKeyTestEvent": {
1448           // eslint-disable-next-line no-undef
1449           MediaControlService.generateMediaControlKey(aMessage.data.event);
1450           return undefined;
1451         }
1453         default:
1454           throw new SpecialPowersError(
1455             `Unrecognized Special Powers API: ${aMessage.name}`
1456           );
1457       }
1458       // This should be unreachable. If it ever becomes reachable, ESLint
1459       // will produce an error about inconsistent return values.
1460     } finally {
1461       if (startTime) {
1462         ChromeUtils.addProfilerMarker(
1463           "SpecialPowers",
1464           { startTime, category: "Test" },
1465           aMessage.name
1466         );
1467       }
1468     }
1469   }