Backed out 2 changesets (bug 1864896) for causing node failures. CLOSED TREE
[gecko.git] / browser / components / newtab / AboutNewTabService.sys.mjs
blobe73e1b188035f574fc86c15aa2bb6b40a73f3da3
1 /**
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5  */
7 /**
8  * The nsIAboutNewTabService is accessed by the AboutRedirector anytime
9  * about:home, about:newtab or about:welcome are requested. The primary
10  * job of an nsIAboutNewTabService is to tell the AboutRedirector what
11  * resources to actually load for those requests.
12  *
13  * The nsIAboutNewTabService is not involved when the user has overridden
14  * the default about:home or about:newtab pages.
15  *
16  * There are two implementations of this service - one for the parent
17  * process, and one for content processes. Each one has some secondary
18  * responsibilties that are process-specific.
19  *
20  * The need for two implementations is an unfortunate consequence of how
21  * document loading and process redirection for about: pages currently
22  * works in Gecko. The commonalities between the two implementations has
23  * been put into an abstract base class.
24  */
26 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
27 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
28 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
30 const lazy = {};
32 ChromeUtils.defineESModuleGetters(lazy, {
33   BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
34   NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
35 });
37 /**
38  * BEWARE: Do not add variables for holding state in the global scope.
39  * Any state variables should be properties of the appropriate class
40  * below. This is to avoid confusion where the state is set in one process,
41  * but not in another.
42  *
43  * Constants are fine in the global scope.
44  */
46 const PREF_ABOUT_HOME_CACHE_TESTING =
47   "browser.startup.homepage.abouthome_cache.testing";
48 const ABOUT_WELCOME_URL =
49   "chrome://browser/content/aboutwelcome/aboutwelcome.html";
51 const CACHE_WORKER_URL = "resource://activity-stream/lib/cache.worker.js";
53 const IS_PRIVILEGED_PROCESS =
54   Services.appinfo.remoteType === E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
56 const PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS =
57   "browser.tabs.remote.separatePrivilegedContentProcess";
58 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
60 /**
61  * The AboutHomeStartupCacheChild is responsible for connecting the
62  * nsIAboutNewTabService with a cached document and script for about:home
63  * if one happens to exist. The AboutHomeStartupCacheChild is only ever
64  * handed the streams for those caches when the "privileged about content
65  * process" first launches, so subsequent loads of about:home do not read
66  * from this cache.
67  *
68  * See https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/v2-system-addon/about_home_startup_cache.html
69  * for further details.
70  */
71 export const AboutHomeStartupCacheChild = {
72   _initted: false,
73   CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest",
74   CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse",
75   CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult",
76   STATES: {
77     UNAVAILABLE: 0,
78     UNCONSUMED: 1,
79     PAGE_CONSUMED: 2,
80     PAGE_AND_SCRIPT_CONSUMED: 3,
81     FAILED: 4,
82     DISQUALIFIED: 5,
83   },
84   REQUEST_TYPE: {
85     PAGE: 0,
86     SCRIPT: 1,
87   },
88   _state: 0,
89   _consumerBCID: null,
91   /**
92    * Called via a process script very early on in the process lifetime. This
93    * prepares the AboutHomeStartupCacheChild to pass an nsIChannel back to
94    * the nsIAboutNewTabService when the initial about:home document is
95    * eventually requested.
96    *
97    * @param pageInputStream (nsIInputStream)
98    *   The stream for the cached page markup.
99    * @param scriptInputStream (nsIInputStream)
100    *   The stream for the cached script to run on the page.
101    */
102   init(pageInputStream, scriptInputStream) {
103     if (
104       !IS_PRIVILEGED_PROCESS &&
105       !Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)
106     ) {
107       throw new Error(
108         "Can only instantiate in the privileged about content processes."
109       );
110     }
112     if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) {
113       return;
114     }
116     if (this._initted) {
117       throw new Error("AboutHomeStartupCacheChild already initted.");
118     }
120     Services.obs.addObserver(this, "memory-pressure");
121     Services.cpmm.addMessageListener(this.CACHE_REQUEST_MESSAGE, this);
123     this._pageInputStream = pageInputStream;
124     this._scriptInputStream = scriptInputStream;
125     this._initted = true;
126     this.setState(this.STATES.UNCONSUMED);
127   },
129   /**
130    * A function that lets us put the AboutHomeStartupCacheChild back into
131    * its initial state. This is used by tests to let us simulate the startup
132    * behaviour of the module without having to manually launch a new privileged
133    * about content process every time.
134    */
135   uninit() {
136     if (!Services.prefs.getBoolPref(PREF_ABOUT_HOME_CACHE_TESTING, false)) {
137       throw new Error(
138         "Cannot uninit AboutHomeStartupCacheChild unless testing."
139       );
140     }
142     if (!this._initted) {
143       return;
144     }
146     Services.obs.removeObserver(this, "memory-pressure");
147     Services.cpmm.removeMessageListener(this.CACHE_REQUEST_MESSAGE, this);
149     if (this._cacheWorker) {
150       this._cacheWorker.terminate();
151       this._cacheWorker = null;
152     }
154     this._pageInputStream = null;
155     this._scriptInputStream = null;
156     this._initted = false;
157     this._state = this.STATES.UNAVAILABLE;
158     this._consumerBCID = null;
159   },
161   /**
162    * A public method called from nsIAboutNewTabService that attempts
163    * return an nsIChannel for a cached about:home document that we
164    * were initialized with. If we failed to be initted with the
165    * cache, or the input streams that we were sent have no data
166    * yet available, this function returns null. The caller should
167    * fall back to generating the page dynamically.
168    *
169    * This function will be called when loading about:home, or
170    * about:home?jscache - the latter returns the cached script.
171    *
172    * It is expected that the same BrowsingContext that loads the cached
173    * page will also load the cached script.
174    *
175    * @param uri (nsIURI)
176    *   The URI for the requested page, as passed by nsIAboutNewTabService.
177    * @param loadInfo (nsILoadInfo)
178    *   The nsILoadInfo for the requested load, as passed by
179    *   nsIAboutNewWTabService.
180    * @return nsIChannel or null.
181    */
182   maybeGetCachedPageChannel(uri, loadInfo) {
183     if (!this._initted) {
184       return null;
185     }
187     if (this._state >= this.STATES.PAGE_AND_SCRIPT_CONSUMED) {
188       return null;
189     }
191     let requestType =
192       uri.query === "jscache"
193         ? this.REQUEST_TYPE.SCRIPT
194         : this.REQUEST_TYPE.PAGE;
196     // If this is a page request, then we need to be in the UNCONSUMED state,
197     // since we expect the page request to come first. If this is a script
198     // request, we expect to be in PAGE_CONSUMED state, since the page cache
199     // stream should he been consumed already.
200     if (
201       (requestType === this.REQUEST_TYPE.PAGE &&
202         this._state !== this.STATES.UNCONSUMED) ||
203       (requestType === this.REQUEST_TYPE_SCRIPT &&
204         this._state !== this.STATES.PAGE_CONSUMED)
205     ) {
206       return null;
207     }
209     // If by this point, we don't have anything in the streams,
210     // then either the cache was too slow to give us data, or the cache
211     // doesn't exist. The caller should fall back to generating the
212     // page dynamically.
213     //
214     // We only do this on the page request, because by the time
215     // we get to the script request, we should have already drained
216     // the page input stream.
217     if (requestType === this.REQUEST_TYPE.PAGE) {
218       try {
219         if (
220           !this._scriptInputStream.available() ||
221           !this._pageInputStream.available()
222         ) {
223           this.setState(this.STATES.FAILED);
224           this.reportUsageResult(false /* success */);
225           return null;
226         }
227       } catch (e) {
228         this.setState(this.STATES.FAILED);
229         if (e.result === Cr.NS_BASE_STREAM_CLOSED) {
230           this.reportUsageResult(false /* success */);
231           return null;
232         }
233         throw e;
234       }
235     }
237     if (
238       requestType === this.REQUEST_TYPE.SCRIPT &&
239       this._consumerBCID !== loadInfo.browsingContextID
240     ) {
241       // Some other document is somehow requesting the script - one
242       // that didn't originally request the page. This is not allowed.
243       this.setState(this.STATES.FAILED);
244       return null;
245     }
247     let channel = Cc[
248       "@mozilla.org/network/input-stream-channel;1"
249     ].createInstance(Ci.nsIInputStreamChannel);
250     channel.QueryInterface(Ci.nsIChannel);
251     channel.setURI(uri);
252     channel.loadInfo = loadInfo;
253     channel.contentStream =
254       requestType === this.REQUEST_TYPE.PAGE
255         ? this._pageInputStream
256         : this._scriptInputStream;
258     if (requestType === this.REQUEST_TYPE.SCRIPT) {
259       this.setState(this.STATES.PAGE_AND_SCRIPT_CONSUMED);
260       this.reportUsageResult(true /* success */);
261     } else {
262       this.setState(this.STATES.PAGE_CONSUMED);
263       // Stash the BrowsingContext ID so that when the script stream
264       // attempts to be consumed, we ensure that it's from the same
265       // BrowsingContext that loaded the page.
266       this._consumerBCID = loadInfo.browsingContextID;
267     }
269     return channel;
270   },
272   /**
273    * This function takes the state information required to generate
274    * the about:home cache markup and script, and then generates that
275    * markup in script asynchronously. Once that's done, a message
276    * is sent to the parent process with the nsIInputStream's for the
277    * markup and script contents.
278    *
279    * @param state (Object)
280    *   The Redux state of the about:home document to render.
281    * @return Promise
282    * @resolves undefined
283    *   After the message with the nsIInputStream's have been sent to
284    *   the parent.
285    */
286   async constructAndSendCache(state) {
287     if (!IS_PRIVILEGED_PROCESS) {
288       throw new Error("Wrong process type.");
289     }
291     let worker = this.getOrCreateWorker();
293     TelemetryStopwatch.start("FX_ABOUTHOME_CACHE_CONSTRUCTION");
295     let { page, script } = await worker
296       .post("construct", [state])
297       .finally(() => {
298         TelemetryStopwatch.finish("FX_ABOUTHOME_CACHE_CONSTRUCTION");
299       });
301     let pageInputStream = Cc[
302       "@mozilla.org/io/string-input-stream;1"
303     ].createInstance(Ci.nsIStringInputStream);
305     pageInputStream.setUTF8Data(page);
307     let scriptInputStream = Cc[
308       "@mozilla.org/io/string-input-stream;1"
309     ].createInstance(Ci.nsIStringInputStream);
311     scriptInputStream.setUTF8Data(script);
313     Services.cpmm.sendAsyncMessage(this.CACHE_RESPONSE_MESSAGE, {
314       pageInputStream,
315       scriptInputStream,
316     });
317   },
319   _cacheWorker: null,
320   getOrCreateWorker() {
321     if (this._cacheWorker) {
322       return this._cacheWorker;
323     }
325     this._cacheWorker = new lazy.BasePromiseWorker(CACHE_WORKER_URL);
326     return this._cacheWorker;
327   },
329   receiveMessage(message) {
330     if (message.name === this.CACHE_REQUEST_MESSAGE) {
331       let { state } = message.data;
332       this.constructAndSendCache(state);
333     }
334   },
336   reportUsageResult(success) {
337     Services.cpmm.sendAsyncMessage(this.CACHE_USAGE_RESULT_MESSAGE, {
338       success,
339     });
340   },
342   observe(subject, topic, data) {
343     if (topic === "memory-pressure" && this._cacheWorker) {
344       this._cacheWorker.terminate();
345       this._cacheWorker = null;
346     }
347   },
349   /**
350    * Transitions the AboutHomeStartupCacheChild from one state
351    * to the next, where each state is defined in this.STATES.
352    *
353    * States can only be transitioned in increasing order, otherwise
354    * an error is logged.
355    */
356   setState(state) {
357     if (state > this._state) {
358       this._state = state;
359     } else {
360       console.error(
361         "AboutHomeStartupCacheChild could not transition from state " +
362           `${this._state} to ${state}`,
363         new Error().stack
364       );
365     }
366   },
368   /**
369    * If the cache hasn't been used, transitions it into the DISQUALIFIED
370    * state so that it cannot be used. This should be called if it's been
371    * determined that about:newtab is going to be loaded, which doesn't
372    * use the cache.
373    */
374   disqualifyCache() {
375     if (this._state === this.STATES.UNCONSUMED) {
376       this.setState(this.STATES.DISQUALIFIED);
377       this.reportUsageResult(false /* success */);
378     }
379   },
383  * This is an abstract base class for the nsIAboutNewTabService
384  * implementations that has some common methods and properties.
385  */
386 class BaseAboutNewTabService {
387   constructor() {
388     if (!AppConstants.RELEASE_OR_BETA) {
389       XPCOMUtils.defineLazyPreferenceGetter(
390         this,
391         "activityStreamDebug",
392         PREF_ACTIVITY_STREAM_DEBUG,
393         false
394       );
395     } else {
396       this.activityStreamDebug = false;
397     }
399     XPCOMUtils.defineLazyPreferenceGetter(
400       this,
401       "privilegedAboutProcessEnabled",
402       PREF_SEPARATE_PRIVILEGEDABOUT_CONTENT_PROCESS,
403       false
404     );
406     this.classID = Components.ID("{cb36c925-3adc-49b3-b720-a5cc49d8a40e}");
407     this.QueryInterface = ChromeUtils.generateQI([
408       "nsIAboutNewTabService",
409       "nsIObserver",
410     ]);
411   }
413   /**
414    * Returns the default URL.
415    *
416    * This URL depends on various activity stream prefs. Overriding
417    * the newtab page has no effect on the result of this function.
418    */
419   get defaultURL() {
420     // Generate the desired activity stream resource depending on state, e.g.,
421     // "resource://activity-stream/prerendered/activity-stream.html"
422     // "resource://activity-stream/prerendered/activity-stream-debug.html"
423     // "resource://activity-stream/prerendered/activity-stream-noscripts.html"
424     return [
425       "resource://activity-stream/prerendered/",
426       "activity-stream",
427       // Debug version loads dev scripts but noscripts separately loads scripts
428       this.activityStreamDebug && !this.privilegedAboutProcessEnabled
429         ? "-debug"
430         : "",
431       this.privilegedAboutProcessEnabled ? "-noscripts" : "",
432       ".html",
433     ].join("");
434   }
436   get welcomeURL() {
437     /*
438      * Returns the about:welcome URL
439      *
440      * This is calculated in the same way the default URL is.
441      */
443     lazy.NimbusFeatures.aboutwelcome.recordExposureEvent({ once: true });
444     if (lazy.NimbusFeatures.aboutwelcome.getVariable("enabled") ?? true) {
445       return ABOUT_WELCOME_URL;
446     }
447     return this.defaultURL;
448   }
450   aboutHomeChannel(uri, loadInfo) {
451     throw Components.Exception(
452       "AboutHomeChannel not implemented for this process.",
453       Cr.NS_ERROR_NOT_IMPLEMENTED
454     );
455   }
459  * The child-process implementation of nsIAboutNewTabService,
460  * which also does the work of redirecting about:home loads to
461  * the about:home startup cache if its available.
462  */
463 class AboutNewTabChildService extends BaseAboutNewTabService {
464   aboutHomeChannel(uri, loadInfo) {
465     if (IS_PRIVILEGED_PROCESS) {
466       let cacheChannel = AboutHomeStartupCacheChild.maybeGetCachedPageChannel(
467         uri,
468         loadInfo
469       );
470       if (cacheChannel) {
471         return cacheChannel;
472       }
473     }
475     let pageURI = Services.io.newURI(this.defaultURL);
476     let fileChannel = Services.io.newChannelFromURIWithLoadInfo(
477       pageURI,
478       loadInfo
479     );
480     fileChannel.originalURI = uri;
481     return fileChannel;
482   }
484   get defaultURL() {
485     if (IS_PRIVILEGED_PROCESS) {
486       // This is a bit of a hack, but attempting to load about:newtab will
487       // enter this code path in order to get at the expected URL, and we
488       // can use that to disqualify the about:home cache, since we don't
489       // use it for about:newtab loads, and we don't want the about:home
490       // cache to be wildly out of date when about:home is eventually
491       // loaded (for example, in the first new window).
492       AboutHomeStartupCacheChild.disqualifyCache();
493     }
495     return super.defaultURL;
496   }
500  * The AboutNewTabStubService is a function called in both the main and
501  * content processes when trying to get at the nsIAboutNewTabService. This
502  * function does the job of choosing the appropriate implementation of
503  * nsIAboutNewTabService for the process type.
504  */
505 export function AboutNewTabStubService() {
506   if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
507     return new BaseAboutNewTabService();
508   }
509   return new AboutNewTabChildService();