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/. */
4
5/*
6 * This module is responsible for uploading pings to the server and persisting
7 * pings that can't be send now.
8 * Those pending pings are persisted on disk and sent at the next opportunity,
9 * newest first.
10 */
11
12"use strict";
13
14var EXPORTED_SYMBOLS = [
15  "TelemetrySend",
16];
17
18ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);
19ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
20ChromeUtils.import("resource://gre/modules/ClientID.jsm");
21ChromeUtils.import("resource://gre/modules/Log.jsm", this);
22ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
23ChromeUtils.import("resource://gre/modules/ServiceRequest.jsm", this);
24ChromeUtils.import("resource://gre/modules/Services.jsm", this);
25ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this);
26ChromeUtils.import("resource://gre/modules/Timer.jsm", this);
27
28ChromeUtils.defineModuleGetter(this, "AsyncShutdown",
29                               "resource://gre/modules/AsyncShutdown.jsm");
30ChromeUtils.defineModuleGetter(this, "TelemetryStorage",
31                               "resource://gre/modules/TelemetryStorage.jsm");
32ChromeUtils.defineModuleGetter(this, "TelemetryReportingPolicy",
33                               "resource://gre/modules/TelemetryReportingPolicy.jsm");
34ChromeUtils.defineModuleGetter(this, "OS",
35                               "resource://gre/modules/osfile.jsm");
36XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
37                                   "@mozilla.org/base/telemetry;1",
38                                   "nsITelemetry");
39ChromeUtils.defineModuleGetter(this, "TelemetryHealthPing",
40                               "resource://gre/modules/TelemetryHealthPing.jsm");
41
42
43const Utils = TelemetryUtils;
44
45const LOGGER_NAME = "Toolkit.Telemetry";
46const LOGGER_PREFIX = "TelemetrySend::";
47
48const TOPIC_IDLE_DAILY = "idle-daily";
49// The following topics are notified when Firefox is closing
50// because the OS is shutting down.
51const TOPIC_QUIT_APPLICATION_GRANTED = "quit-application-granted";
52const TOPIC_QUIT_APPLICATION_FORCED = "quit-application-forced";
53const PREF_CHANGED_TOPIC = "nsPref:changed";
54const TOPIC_PROFILE_CHANGE_NET_TEARDOWN = "profile-change-net-teardown";
55
56// Whether the FHR/Telemetry unification features are enabled.
57// Changing this pref requires a restart.
58const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(TelemetryUtils.Preferences.Unified, false);
59
60const PING_FORMAT_VERSION = 4;
61
62const MS_IN_A_MINUTE = 60 * 1000;
63
64const PING_TYPE_DELETION = "deletion";
65
66// We try to spread "midnight" pings out over this interval.
67const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * MS_IN_A_MINUTE;
68// We delay sending "midnight" pings on this client by this interval.
69const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS;
70
71// Timeout after which we consider a ping submission failed.
72const PING_SUBMIT_TIMEOUT_MS = 1.5 * MS_IN_A_MINUTE;
73
74// To keep resource usage in check, we limit ping sending to a maximum number
75// of pings per minute.
76const MAX_PING_SENDS_PER_MINUTE = 10;
77
78// If we have more pending pings then we can send right now, we schedule the next
79// send for after SEND_TICK_DELAY.
80const SEND_TICK_DELAY = 1 * MS_IN_A_MINUTE;
81// If we had any ping send failures since the last ping, we use a backoff timeout
82// for the next ping sends. We increase the delay exponentially up to a limit of
83// SEND_MAXIMUM_BACKOFF_DELAY_MS.
84// This exponential backoff will be reset by external ping submissions & idle-daily.
85const SEND_MAXIMUM_BACKOFF_DELAY_MS = 120 * MS_IN_A_MINUTE;
86
87// The age of a pending ping to be considered overdue (in milliseconds).
88const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * MS_IN_A_MINUTE; // 1 week
89
90// Strings to map from XHR.errorCode to TELEMETRY_SEND_FAILURE_TYPE.
91// Echoes XMLHttpRequestMainThread's ErrorType enum.
92const XHR_ERROR_TYPE = [
93  "eOK",
94  "eRequest",
95  "eUnreachable",
96  "eChannelOpen",
97  "eRedirect",
98];
99
100/**
101 * This is a policy object used to override behavior within this module.
102 * Tests override properties on this object to allow for control of behavior
103 * that would otherwise be very hard to cover.
104 */
105var Policy = {
106  now: () => new Date(),
107  midnightPingFuzzingDelay: () => MIDNIGHT_FUZZING_DELAY_MS,
108  pingSubmissionTimeout: () => PING_SUBMIT_TIMEOUT_MS,
109  setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
110  clearSchedulerTickTimeout: (id) => clearTimeout(id),
111};
112
113/**
114 * Determine if the ping has the new v4 ping format or the legacy v2 one or earlier.
115 */
116function isV4PingFormat(aPing) {
117  return ("id" in aPing) && ("application" in aPing) &&
118         ("version" in aPing) && (aPing.version >= 2);
119}
120
121/**
122 * Check if the provided ping is a deletion ping.
123 * @param {Object} aPing The ping to check.
124 * @return {Boolean} True if the ping is a deletion ping, false otherwise.
125 */
126function isDeletionPing(aPing) {
127  return isV4PingFormat(aPing) && (aPing.type == PING_TYPE_DELETION);
128}
129
130/**
131 * Save the provided ping as a pending ping. If it's a deletion ping, save it
132 * to a special location.
133 * @param {Object} aPing The ping to save.
134 * @return {Promise} A promise resolved when the ping is saved.
135 */
136function savePing(aPing) {
137  if (isDeletionPing(aPing)) {
138    return TelemetryStorage.saveDeletionPing(aPing);
139  }
140  return TelemetryStorage.savePendingPing(aPing);
141}
142
143/**
144 * @return {String} This returns a string with the gzip compressed data.
145 */
146function gzipCompressString(string) {
147  let observer = {
148    buffer: "",
149    onStreamComplete(loader, context, status, length, result) {
150      // String.fromCharCode can only deal with 500,000 characters at
151      // a time, so chunk the result into parts of that size.
152      const chunkSize = 500000;
153      for (let offset = 0; offset < result.length; offset += chunkSize) {
154        this.buffer += String.fromCharCode.apply(String, result.slice(offset, offset + chunkSize));
155      }
156    }
157  };
158
159  let scs = Cc["@mozilla.org/streamConverters;1"]
160            .getService(Ci.nsIStreamConverterService);
161  let listener = Cc["@mozilla.org/network/stream-loader;1"]
162                .createInstance(Ci.nsIStreamLoader);
163  listener.init(observer);
164  let converter = scs.asyncConvertData("uncompressed", "gzip",
165                                       listener, null);
166  let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
167                     .createInstance(Ci.nsIStringInputStream);
168  stringStream.data = string;
169  converter.onStartRequest(null, null);
170  converter.onDataAvailable(null, null, stringStream, 0, string.length);
171  converter.onStopRequest(null, null, null);
172  return observer.buffer;
173}
174
175var TelemetrySend = {
176
177  /**
178   * Age in ms of a pending ping to be considered overdue.
179   */
180  get OVERDUE_PING_FILE_AGE() {
181    return OVERDUE_PING_FILE_AGE;
182  },
183
184  get pendingPingCount() {
185    return TelemetrySendImpl.pendingPingCount;
186  },
187
188  /**
189   * Partial setup that runs immediately at startup. This currently triggers
190   * the crash report annotations.
191   */
192  earlyInit() {
193    TelemetrySendImpl.earlyInit();
194  },
195
196  /**
197   * Initializes this module.
198   *
199   * @param {Boolean} testing Whether this is run in a test. This changes some behavior
200   * to enable proper testing.
201   * @return {Promise} Resolved when setup is finished.
202   */
203  setup(testing = false) {
204    return TelemetrySendImpl.setup(testing);
205  },
206
207  /**
208   * Shutdown this module - this will cancel any pending ping tasks and wait for
209   * outstanding async activity like network and disk I/O.
210   *
211   * @return {Promise} Promise that is resolved when shutdown is finished.
212   */
213  shutdown() {
214    return TelemetrySendImpl.shutdown();
215  },
216
217  /**
218   * Submit a ping for sending. This will:
219   * - send the ping right away if possible or
220   * - save the ping to disk and send it at the next opportunity
221   *
222   * @param {Object} ping The ping data to send, must be serializable to JSON.
223   * @param {Object} [aOptions] Options object.
224   * @param {Boolean} [options.usePingSender=false] if true, send the ping using the PingSender.
225   * @return {Promise} Test-only - a promise that is resolved when the ping is sent or saved.
226   */
227  submitPing(ping, options = {}) {
228    options.usePingSender = options.usePingSender || false;
229    return TelemetrySendImpl.submitPing(ping, options);
230  },
231
232  /**
233   * Count of pending pings that were found to be overdue at startup.
234   */
235  get overduePingsCount() {
236    return TelemetrySendImpl.overduePingsCount;
237  },
238
239  /**
240   * Notify that we can start submitting data to the servers.
241   */
242  notifyCanUpload() {
243    return TelemetrySendImpl.notifyCanUpload();
244  },
245
246  /**
247   * Only used in tests. Used to reset the module data to emulate a restart.
248   */
249  reset() {
250    return TelemetrySendImpl.reset();
251  },
252
253  /**
254   * Only used in tests.
255   */
256  setServer(server) {
257    return TelemetrySendImpl.setServer(server);
258  },
259
260  /**
261   * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
262   */
263  clearCurrentPings() {
264    return TelemetrySendImpl.clearCurrentPings();
265  },
266
267  /**
268   * Only used in tests to wait on outgoing pending pings.
269   */
270  testWaitOnOutgoingPings() {
271    return TelemetrySendImpl.promisePendingPingActivity();
272  },
273
274  /**
275   * Only used in tests to set whether it is too late in shutdown to send pings.
276   */
277  testTooLateToSend(tooLate) {
278    TelemetrySendImpl._tooLateToSend = tooLate;
279  },
280
281  /**
282   * Test-only - this allows overriding behavior to enable ping sending in debug builds.
283   */
284  setTestModeEnabled(testing) {
285    TelemetrySendImpl.setTestModeEnabled(testing);
286  },
287
288  /**
289   * This returns state info for this module for AsyncShutdown timeout diagnostics.
290   */
291  getShutdownState() {
292    return TelemetrySendImpl.getShutdownState();
293  },
294
295  /**
296   * Send a ping using the ping sender.
297   * This method will not wait for the ping to be sent, instead it will return
298   * as soon as the pingsender program has been launched.
299   *
300   * This method is currently exposed here only for testing purposes as it's
301   * only used internally.
302   *
303   * @param {String} aUrl The telemetry server URL
304   * @param {String} aPingFilePath The path to the file holding the ping
305   *        contents, if if sent successfully the pingsender will delete it.
306   *
307   * @throws NS_ERROR_FAILURE if we couldn't find or run the pingsender
308   *         executable.
309   * @throws NS_ERROR_NOT_IMPLEMENTED on Android as the pingsender is not
310   *         available.
311   */
312  testRunPingSender(url, pingPath) {
313    TelemetrySendImpl.runPingSender(url, pingPath);
314  },
315};
316
317var CancellableTimeout = {
318  _deferred: null,
319  _timer: null,
320
321  /**
322   * This waits until either the given timeout passed or the timeout was cancelled.
323   *
324   * @param {Number} timeoutMs The timeout in ms.
325   * @return {Promise<bool>} Promise that is resolved with false if the timeout was cancelled,
326   *                         false otherwise.
327   */
328  promiseWaitOnTimeout(timeoutMs) {
329    if (!this._deferred) {
330      this._deferred = PromiseUtils.defer();
331      this._timer = Policy.setSchedulerTickTimeout(() => this._onTimeout(), timeoutMs);
332    }
333
334    return this._deferred.promise;
335  },
336
337  _onTimeout() {
338    if (this._deferred) {
339      this._deferred.resolve(false);
340      this._timer = null;
341      this._deferred = null;
342    }
343  },
344
345  cancelTimeout() {
346    if (this._deferred) {
347      Policy.clearSchedulerTickTimeout(this._timer);
348      this._deferred.resolve(true);
349      this._timer = null;
350      this._deferred = null;
351    }
352  },
353};
354
355/**
356 * SendScheduler implements the timer & scheduling behavior for ping sends.
357 */
358var SendScheduler = {
359  // Whether any ping sends failed since the last tick. If yes, we start with our exponential
360  // backoff timeout.
361  _sendsFailed: false,
362  // The current retry delay after ping send failures. We use this for the exponential backoff,
363  // increasing this value everytime we had send failures since the last tick.
364  _backoffDelay: SEND_TICK_DELAY,
365  _shutdown: false,
366  _sendTask: null,
367  // A string that tracks the last seen send task state, null if it never ran.
368  _sendTaskState: null,
369
370  _logger: null,
371
372  get _log() {
373    if (!this._logger) {
374      this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX + "Scheduler::");
375    }
376
377    return this._logger;
378  },
379
380  shutdown() {
381    this._log.trace("shutdown");
382    this._shutdown = true;
383    CancellableTimeout.cancelTimeout();
384    return Promise.resolve(this._sendTask);
385  },
386
387  start() {
388    this._log.trace("start");
389    this._sendsFailed = false;
390    this._backoffDelay = SEND_TICK_DELAY;
391    this._shutdown = false;
392  },
393
394  /**
395   * Only used for testing, resets the state to emulate a restart.
396   */
397  reset() {
398    this._log.trace("reset");
399    return this.shutdown().then(() => this.start());
400  },
401
402  /**
403   * Notify the scheduler of a failure in sending out pings that warrants retrying.
404   * This will trigger the exponential backoff timer behavior on the next tick.
405   */
406  notifySendsFailed() {
407    this._log.trace("notifySendsFailed");
408    if (this._sendsFailed) {
409      return;
410    }
411
412    this._sendsFailed = true;
413    this._log.trace("notifySendsFailed - had send failures");
414  },
415
416  /**
417   * Returns whether ping submissions are currently throttled.
418   */
419  isThrottled() {
420    const now = Policy.now();
421    const nextPingSendTime = this._getNextPingSendTime(now);
422    return (nextPingSendTime > now.getTime());
423  },
424
425  waitOnSendTask() {
426    return Promise.resolve(this._sendTask);
427  },
428
429  triggerSendingPings(immediately) {
430    this._log.trace("triggerSendingPings - active send task: " + !!this._sendTask + ", immediately: " + immediately);
431
432    if (!this._sendTask) {
433      this._sendTask = this._doSendTask();
434      let clear = () => this._sendTask = null;
435      this._sendTask.then(clear, clear);
436    } else if (immediately) {
437      CancellableTimeout.cancelTimeout();
438    }
439
440    return this._sendTask;
441  },
442
443  async _doSendTask() {
444    this._sendTaskState = "send task started";
445    this._backoffDelay = SEND_TICK_DELAY;
446    this._sendsFailed = false;
447
448    const resetBackoffTimer = () => {
449      this._backoffDelay = SEND_TICK_DELAY;
450    };
451
452    for (;;) {
453      this._log.trace("_doSendTask iteration");
454      this._sendTaskState = "start iteration";
455
456      if (this._shutdown) {
457        this._log.trace("_doSendTask - shutting down, bailing out");
458        this._sendTaskState = "bail out - shutdown check";
459        return;
460      }
461
462      // Get a list of pending pings, sorted by last modified, descending.
463      // Filter out all the pings we can't send now. This addresses scenarios like "deletion" pings
464      // which can be send even when upload is disabled.
465      let pending = TelemetryStorage.getPendingPingList();
466      let current = TelemetrySendImpl.getUnpersistedPings();
467      this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length);
468      // Note that the two lists contain different kind of data. |pending| only holds ping
469      // info, while |current| holds actual ping data.
470      if (!TelemetrySendImpl.sendingEnabled()) {
471        pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id));
472        current = current.filter(p => isDeletionPing(p));
473      }
474      this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length);
475
476      // Bail out if there is nothing to send.
477      if ((pending.length == 0) && (current.length == 0)) {
478        this._log.trace("_doSendTask - no pending pings, bailing out");
479        this._sendTaskState = "bail out - no pings to send";
480        return;
481      }
482
483      // If we are currently throttled (e.g. fuzzing to avoid midnight spikes), wait for the next send window.
484      const now = Policy.now();
485      if (this.isThrottled()) {
486        const nextPingSendTime = this._getNextPingSendTime(now);
487        this._log.trace("_doSendTask - throttled, delaying ping send to " + new Date(nextPingSendTime));
488        this._sendTaskState = "wait for throttling to pass";
489
490        const delay = nextPingSendTime - now.getTime();
491        const cancelled = await CancellableTimeout.promiseWaitOnTimeout(delay);
492        if (cancelled) {
493          this._log.trace("_doSendTask - throttling wait was cancelled, resetting backoff timer");
494          resetBackoffTimer();
495        }
496
497        continue;
498      }
499
500      let sending = pending.slice(0, MAX_PING_SENDS_PER_MINUTE);
501      pending = pending.slice(MAX_PING_SENDS_PER_MINUTE);
502      this._log.trace("_doSendTask - triggering sending of " + sending.length + " pings now" +
503                      ", " + pending.length + " pings waiting");
504
505      this._sendsFailed = false;
506      const sendStartTime = Policy.now();
507      this._sendTaskState = "wait on ping sends";
508      await TelemetrySendImpl.sendPings(current, sending.map(p => p.id));
509      if (this._shutdown || (TelemetrySend.pendingPingCount == 0)) {
510        this._log.trace("_doSendTask - bailing out after sending, shutdown: " + this._shutdown +
511                        ", pendingPingCount: " + TelemetrySend.pendingPingCount);
512        this._sendTaskState = "bail out - shutdown & pending check after send";
513        return;
514      }
515
516      // Calculate the delay before sending the next batch of pings.
517      // We start with a delay that makes us send max. 1 batch per minute.
518      // If we had send failures in the last batch, we will override this with
519      // a backoff delay.
520      const timeSinceLastSend = Policy.now() - sendStartTime;
521      let nextSendDelay = Math.max(0, SEND_TICK_DELAY - timeSinceLastSend);
522
523      if (!this._sendsFailed) {
524        this._log.trace("_doSendTask - had no send failures, resetting backoff timer");
525        resetBackoffTimer();
526      } else {
527        const newDelay = Math.min(SEND_MAXIMUM_BACKOFF_DELAY_MS,
528                                  this._backoffDelay * 2);
529        this._log.trace("_doSendTask - had send failures, backing off -" +
530                        " old timeout: " + this._backoffDelay +
531                        ", new timeout: " + newDelay);
532        this._backoffDelay = newDelay;
533        nextSendDelay = this._backoffDelay;
534      }
535
536      this._log.trace("_doSendTask - waiting for next send opportunity, timeout is " + nextSendDelay);
537      this._sendTaskState = "wait on next send opportunity";
538      const cancelled = await CancellableTimeout.promiseWaitOnTimeout(nextSendDelay);
539      if (cancelled) {
540        this._log.trace("_doSendTask - batch send wait was cancelled, resetting backoff timer");
541        resetBackoffTimer();
542      }
543    }
544  },
545
546  /**
547   * This helper calculates the next time that we can send pings at.
548   * Currently this mostly redistributes ping sends from midnight until one hour after
549   * to avoid submission spikes around local midnight for daily pings.
550   *
551   * @param now Date The current time.
552   * @return Number The next time (ms from UNIX epoch) when we can send pings.
553   */
554  _getNextPingSendTime(now) {
555    // 1. First we check if the time is between 0am and 1am. If it's not, we send
556    // immediately.
557    // 2. If we confirmed the time is indeed between 0am and 1am in step 1, we disallow
558    // sending before (midnight + fuzzing delay), which is a random time between 0am-1am
559    // (decided at startup).
560
561    const midnight = Utils.truncateToDays(now);
562    // Don't delay pings if we are not within the fuzzing interval.
563    if ((now.getTime() - midnight.getTime()) > MIDNIGHT_FUZZING_INTERVAL_MS) {
564      return now.getTime();
565    }
566
567    // Delay ping send if we are within the midnight fuzzing range.
568    // We spread those ping sends out between |midnight| and |midnight + midnightPingFuzzingDelay|.
569    return midnight.getTime() + Policy.midnightPingFuzzingDelay();
570  },
571
572  getShutdownState() {
573    return {
574      shutdown: this._shutdown,
575      hasSendTask: !!this._sendTask,
576      sendsFailed: this._sendsFailed,
577      sendTaskState: this._sendTaskState,
578      backoffDelay: this._backoffDelay,
579    };
580  },
581 };
582
583var TelemetrySendImpl = {
584  _sendingEnabled: false,
585  // Tracks the shutdown state.
586  _shutdown: false,
587  _logger: null,
588  // This tracks all pending ping requests to the server.
589  _pendingPingRequests: new Map(),
590  // This tracks all the pending async ping activity.
591  _pendingPingActivity: new Set(),
592  // This is true when running in the test infrastructure.
593  _testMode: false,
594  // This holds pings that we currently try and haven't persisted yet.
595  _currentPings: new Map(),
596  // Used to skip spawning the pingsender if OS is shutting down.
597  _isOSShutdown: false,
598  // Count of pending pings that were overdue.
599  _overduePingCount: 0,
600  // Has the network shut down, making it too late to send pings?
601  _tooLateToSend: false,
602
603  OBSERVER_TOPICS: [
604    TOPIC_IDLE_DAILY,
605    TOPIC_QUIT_APPLICATION_GRANTED,
606    TOPIC_QUIT_APPLICATION_FORCED,
607    TOPIC_PROFILE_CHANGE_NET_TEARDOWN,
608  ],
609
610  OBSERVED_PREFERENCES: [
611    TelemetryUtils.Preferences.TelemetryEnabled,
612    TelemetryUtils.Preferences.FhrUploadEnabled,
613  ],
614
615  // Whether sending pings has been overridden.
616  get _overrideOfficialCheck() {
617    return Services.prefs.getBoolPref(TelemetryUtils.Preferences.OverrideOfficialCheck, false);
618  },
619
620  get _log() {
621    if (!this._logger) {
622      this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
623    }
624
625    return this._logger;
626  },
627
628  get overduePingsCount() {
629    return this._overduePingCount;
630  },
631
632  get pendingPingRequests() {
633    return this._pendingPingRequests;
634  },
635
636  get pendingPingCount() {
637    return TelemetryStorage.getPendingPingList().length + this._currentPings.size;
638  },
639
640  setTestModeEnabled(testing) {
641    this._testMode = testing;
642  },
643
644  earlyInit() {
645    this._annotateCrashReport();
646
647    // Install the observer to detect OS shutdown early enough, so
648    // that we catch this before the delayed setup happens.
649    Services.obs.addObserver(this, TOPIC_QUIT_APPLICATION_FORCED);
650    Services.obs.addObserver(this, TOPIC_QUIT_APPLICATION_GRANTED);
651  },
652
653  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference]),
654
655  async setup(testing) {
656    this._log.trace("setup");
657
658    this._testMode = testing;
659
660    Services.obs.addObserver(this, TOPIC_IDLE_DAILY);
661    Services.obs.addObserver(this, TOPIC_PROFILE_CHANGE_NET_TEARDOWN);
662
663    this._server = Services.prefs.getStringPref(TelemetryUtils.Preferences.Server, undefined);
664    this._sendingEnabled = true;
665
666    // Annotate crash reports so that crash pings are sent correctly and listen
667    // to pref changes to adjust the annotations accordingly.
668    for (let pref of this.OBSERVED_PREFERENCES) {
669      Services.prefs.addObserver(pref, this, true);
670    }
671    this._annotateCrashReport();
672
673    // Check the pending pings on disk now.
674    try {
675      await this._checkPendingPings();
676    } catch (ex) {
677      this._log.error("setup - _checkPendingPings rejected", ex);
678    }
679
680    // Enforce the pending pings storage quota. It could take a while so don't
681    // block on it.
682    TelemetryStorage.runEnforcePendingPingsQuotaTask();
683
684    // Start sending pings, but don't block on this.
685    SendScheduler.triggerSendingPings(true);
686  },
687
688  /**
689   * Triggers the crash report annotations depending on the current
690   * configuration. This communicates to the crash reporter if it can send a
691   * crash ping or not. This method can be called safely before setup() has
692   * been called.
693   */
694  _annotateCrashReport() {
695    try {
696      const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"];
697      if (cr) {
698        const crs = cr.getService(Ci.nsICrashReporter);
699
700        let clientId = ClientID.getCachedClientID();
701        let server = this._server || Services.prefs.getStringPref(TelemetryUtils.Preferences.Server, undefined);
702
703        if (!this.sendingEnabled() || !TelemetryReportingPolicy.canUpload()) {
704          // If we cannot send pings then clear the crash annotations
705          crs.annotateCrashReport("TelemetryClientId", "");
706          crs.annotateCrashReport("TelemetryServerURL", "");
707        } else {
708          crs.annotateCrashReport("TelemetryClientId", clientId);
709          crs.annotateCrashReport("TelemetryServerURL", server);
710        }
711      }
712    } catch (e) {
713      // Ignore errors when crash reporting is disabled
714    }
715  },
716
717  /**
718   * Discard old pings from the pending pings and detect overdue ones.
719   * @return {Boolean} True if we have overdue pings, false otherwise.
720   */
721  async _checkPendingPings() {
722    // Scan the pending pings - that gives us a list sorted by last modified, descending.
723    let infos = await TelemetryStorage.loadPendingPingList();
724    this._log.info("_checkPendingPings - pending ping count: " + infos.length);
725    if (infos.length == 0) {
726      this._log.trace("_checkPendingPings - no pending pings");
727      return;
728    }
729
730    const now = Policy.now();
731
732    // Check for overdue pings.
733    const overduePings = infos.filter((info) =>
734      (now.getTime() - info.lastModificationDate) > OVERDUE_PING_FILE_AGE);
735    this._overduePingCount = overduePings.length;
736
737    // Submit the age of the pending pings.
738    for (let pingInfo of infos) {
739      const ageInDays =
740        Utils.millisecondsToDays(Math.abs(now.getTime() - pingInfo.lastModificationDate));
741      Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_AGE").add(ageInDays);
742    }
743   },
744
745  async shutdown() {
746    this._shutdown = true;
747
748    for (let pref of this.OBSERVED_PREFERENCES) {
749      // FIXME: When running tests this causes errors to be printed out if
750      // TelemetrySend.shutdown() is called twice in a row without calling
751      // TelemetrySend.setup() in-between.
752      Services.prefs.removeObserver(pref, this);
753    }
754
755    for (let topic of this.OBSERVER_TOPICS) {
756      try {
757        Services.obs.removeObserver(this, topic);
758      } catch (ex) {
759        this._log.error("shutdown - failed to remove observer for " + topic, ex);
760      }
761    }
762
763    // We can't send anymore now.
764    this._sendingEnabled = false;
765
766    // Cancel any outgoing requests.
767    await this._cancelOutgoingRequests();
768
769    // Stop any active send tasks.
770    await SendScheduler.shutdown();
771
772    // Wait for any outstanding async ping activity.
773    await this.promisePendingPingActivity();
774
775    // Save any outstanding pending pings to disk.
776    await this._persistCurrentPings();
777  },
778
779  reset() {
780    this._log.trace("reset");
781
782    this._shutdown = false;
783    this._currentPings = new Map();
784    this._overduePingCount = 0;
785    this._tooLateToSend = false;
786    this._isOSShutdown = false;
787    this._sendingEnabled = true;
788
789    const histograms = [
790      "TELEMETRY_SUCCESS",
791      "TELEMETRY_SEND_SUCCESS",
792      "TELEMETRY_SEND_FAILURE",
793      "TELEMETRY_SEND_FAILURE_TYPE",
794    ];
795
796    histograms.forEach(h => Telemetry.getHistogramById(h).clear());
797
798    return SendScheduler.reset();
799  },
800
801  /**
802   * Notify that we can start submitting data to the servers.
803   */
804  notifyCanUpload() {
805    if (!this._sendingEnabled) {
806      this._log.trace("notifyCanUpload - notifying before sending is enabled. Ignoring.");
807      return Promise.resolve();
808    }
809    // Let the scheduler trigger sending pings if possible, also inform the
810    // crash reporter that it can send crash pings if appropriate.
811    SendScheduler.triggerSendingPings(true);
812    this._annotateCrashReport();
813
814    return this.promisePendingPingActivity();
815  },
816
817  observe(subject, topic, data) {
818    let setOSShutdown = () => {
819      this._log.trace("setOSShutdown - in OS shutdown");
820      this._isOSShutdown = true;
821    };
822
823    switch (topic) {
824    case TOPIC_IDLE_DAILY:
825      SendScheduler.triggerSendingPings(true);
826      break;
827    case TOPIC_QUIT_APPLICATION_FORCED:
828      setOSShutdown();
829      break;
830    case TOPIC_QUIT_APPLICATION_GRANTED:
831      if (data == "syncShutdown") {
832        setOSShutdown();
833      }
834      break;
835    case PREF_CHANGED_TOPIC:
836      if (this.OBSERVED_PREFERENCES.includes(data)) {
837        this._annotateCrashReport();
838      }
839      break;
840    case TOPIC_PROFILE_CHANGE_NET_TEARDOWN:
841      this._tooLateToSend = true;
842      break;
843    }
844  },
845
846  /**
847   * Spawn the PingSender process that sends a ping. This function does
848   * not return an error or throw, it only logs an error.
849   *
850   * Even if the function doesn't fail, it doesn't mean that the ping was
851   * successfully sent, as we have no control over the spawned process. If it,
852   * succeeds, the ping is eventually removed from the disk to prevent duplicated
853   * submissions.
854   *
855   * @param {String} pingId The id of the ping to send.
856   * @param {String} submissionURL The complete Telemetry-compliant URL for the ping.
857   */
858  _sendWithPingSender(pingId, submissionURL) {
859    this._log.trace("_sendWithPingSender - sending " + pingId + " to " + submissionURL);
860    try {
861      const pingPath = OS.Path.join(TelemetryStorage.pingDirectoryPath, pingId);
862      this.runPingSender(submissionURL, pingPath);
863    } catch (e) {
864      this._log.error("_sendWithPingSender - failed to submit ping", e);
865    }
866  },
867
868  submitPing(ping, options) {
869    this._log.trace("submitPing - ping id: " + ping.id + ", options: " + JSON.stringify(options));
870
871    if (!this.sendingEnabled(ping)) {
872      this._log.trace("submitPing - Telemetry is not allowed to send pings.");
873      return Promise.resolve();
874    }
875
876    // Send the ping using the PingSender, if requested and the user was
877    // notified of our policy. We don't support the pingsender on Android,
878    // so ignore this option on that platform (see bug 1335917).
879    // Moreover, if the OS is shutting down, we don't want to spawn the
880    // pingsender as it could unnecessarily slow down OS shutdown.
881    // Additionally, it could be be killed before it can complete its tasks,
882    // for example after successfully sending the ping but before removing
883    // the copy from the disk, resulting in receiving duplicate pings when
884    // Firefox restarts.
885    if (options.usePingSender &&
886        !this._isOSShutdown &&
887        TelemetryReportingPolicy.canUpload() &&
888        AppConstants.platform != "android") {
889      const url = this._buildSubmissionURL(ping);
890      // Serialize the ping to the disk and then spawn the PingSender.
891      return savePing(ping).then(() => this._sendWithPingSender(ping.id, url));
892    }
893
894    if (!this.canSendNow) {
895      // Sending is disabled or throttled, add this to the persisted pending pings.
896      this._log.trace("submitPing - can't send ping now, persisting to disk - " +
897                      "canSendNow: " + this.canSendNow);
898      return savePing(ping);
899    }
900
901    // Let the scheduler trigger sending pings if possible.
902    // As a safety mechanism, this resets any currently active throttling.
903    this._log.trace("submitPing - can send pings, trying to send now");
904    this._currentPings.set(ping.id, ping);
905    SendScheduler.triggerSendingPings(true);
906    return Promise.resolve();
907  },
908
909  /**
910   * Only used in tests.
911   */
912  setServer(server) {
913    this._log.trace("setServer", server);
914    this._server = server;
915  },
916
917  /**
918   * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
919   */
920  async clearCurrentPings() {
921    if (this._shutdown) {
922      this._log.trace("clearCurrentPings - in shutdown, bailing out");
923      return;
924    }
925
926    // Temporarily disable the scheduler. It must not try to reschedule ping sending
927    // while we're deleting them.
928    await SendScheduler.shutdown();
929
930    // Now that the ping activity has settled, abort outstanding ping requests.
931    this._cancelOutgoingRequests();
932
933    // Also, purge current pings.
934    this._currentPings.clear();
935
936    // We might have been interrupted and shutdown could have been started.
937    // We need to bail out in that case to avoid triggering send activity etc.
938    // at unexpected times.
939    if (this._shutdown) {
940      this._log.trace("clearCurrentPings - in shutdown, not spinning SendScheduler up again");
941      return;
942    }
943
944    // Enable the scheduler again and spin the send task.
945    SendScheduler.start();
946    SendScheduler.triggerSendingPings(true);
947  },
948
949  _cancelOutgoingRequests() {
950    // Abort any pending ping XHRs.
951    for (let [id, request] of this._pendingPingRequests) {
952      this._log.trace("_cancelOutgoingRequests - aborting ping request for id " + id);
953      try {
954        request.abort();
955      } catch (e) {
956        this._log.error("_cancelOutgoingRequests - failed to abort request for id " + id, e);
957      }
958    }
959    this._pendingPingRequests.clear();
960  },
961
962  sendPings(currentPings, persistedPingIds) {
963    let pingSends = [];
964
965    // Prioritize health pings to enable low-latency monitoring.
966    currentPings = [
967      ...currentPings.filter(ping => ping.type === "health"),
968      ...currentPings.filter(ping => ping.type !== "health"),
969    ];
970
971    for (let current of currentPings) {
972      let ping = current;
973      let p = (async () => {
974        try {
975          await this._doPing(ping, ping.id, false);
976        } catch (ex) {
977          this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex);
978          // Deletion pings must be saved to a special location.
979          await savePing(ping);
980        } finally {
981          this._currentPings.delete(ping.id);
982        }
983      })();
984
985      this._trackPendingPingTask(p);
986      pingSends.push(p);
987    }
988
989    if (persistedPingIds.length > 0) {
990      pingSends.push(this._sendPersistedPings(persistedPingIds).catch((ex) => {
991        this._log.info("sendPings - persisted pings not sent", ex);
992      }));
993    }
994
995    return Promise.all(pingSends);
996  },
997
998  /**
999   * Send the persisted pings to the server.
1000   *
1001   * @param {Array<string>} List of ping ids that should be sent.
1002   *
1003   * @return Promise A promise that is resolved when all pings finished sending or failed.
1004   */
1005  async _sendPersistedPings(pingIds) {
1006    this._log.trace("sendPersistedPings");
1007
1008    if (TelemetryStorage.pendingPingCount < 1) {
1009      this._log.trace("_sendPersistedPings - no pings to send");
1010      return;
1011    }
1012
1013    if (pingIds.length < 1) {
1014      this._log.trace("sendPersistedPings - no pings to send");
1015      return;
1016    }
1017
1018    // We can send now.
1019    // If there are any send failures, _doPing() sets up handlers that e.g. trigger backoff timer behavior.
1020    this._log.trace("sendPersistedPings - sending " + pingIds.length + " pings");
1021    let pingSendPromises = [];
1022    for (let pingId of pingIds) {
1023      const id = pingId;
1024      pingSendPromises.push(
1025        TelemetryStorage.loadPendingPing(id)
1026          .then((data) => this._doPing(data, id, true))
1027          .catch(e => this._log.error("sendPersistedPings - failed to send ping " + id, e)));
1028    }
1029
1030    let promise = Promise.all(pingSendPromises);
1031    this._trackPendingPingTask(promise);
1032    await promise;
1033  },
1034
1035  _onPingRequestFinished(success, startTime, id, isPersisted) {
1036    this._log.trace("_onPingRequestFinished - success: " + success + ", persisted: " + isPersisted);
1037
1038    let sendId = success ? "TELEMETRY_SEND_SUCCESS" : "TELEMETRY_SEND_FAILURE";
1039    let hsend = Telemetry.getHistogramById(sendId);
1040    let hsuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
1041
1042    hsend.add(Utils.monotonicNow() - startTime);
1043    hsuccess.add(success);
1044
1045    if (!success) {
1046      // Let the scheduler know about send failures for triggering backoff timeouts.
1047      SendScheduler.notifySendsFailed();
1048    }
1049
1050    if (success && isPersisted) {
1051      if (TelemetryStorage.isDeletionPing(id)) {
1052        return TelemetryStorage.removeDeletionPing();
1053      }
1054      return TelemetryStorage.removePendingPing(id);
1055    }
1056    return Promise.resolve();
1057  },
1058
1059  _buildSubmissionURL(ping) {
1060    const version = isV4PingFormat(ping) ? PING_FORMAT_VERSION : 1;
1061    return this._server + this._getSubmissionPath(ping) + "?v=" + version;
1062  },
1063
1064  _getSubmissionPath(ping) {
1065    // The new ping format contains an "application" section, the old one doesn't.
1066    let pathComponents;
1067    if (isV4PingFormat(ping)) {
1068      // We insert the Ping id in the URL to simplify server handling of duplicated
1069      // pings.
1070      let app = ping.application;
1071      pathComponents = [
1072        ping.id, ping.type, app.name, app.version, app.channel, app.buildId
1073      ];
1074    } else {
1075      // This is a ping in the old format.
1076      if (!("slug" in ping)) {
1077        // That's odd, we don't have a slug. Generate one so that TelemetryStorage.jsm works.
1078        ping.slug = Utils.generateUUID();
1079      }
1080
1081      // Do we have enough info to build a submission URL?
1082      let payload = ("payload" in ping) ? ping.payload : null;
1083      if (payload && ("info" in payload)) {
1084        let info = ping.payload.info;
1085        pathComponents = [ ping.slug, info.reason, info.appName, info.appVersion,
1086                           info.appUpdateChannel, info.appBuildID ];
1087      } else {
1088        // Only use the UUID as the slug.
1089        pathComponents = [ ping.slug ];
1090      }
1091    }
1092
1093    let slug = pathComponents.join("/");
1094    return "/submit/telemetry/" + slug;
1095  },
1096
1097  _doPing(ping, id, isPersisted) {
1098    if (!this.sendingEnabled(ping)) {
1099      // We can't send the pings to the server, so don't try to.
1100      this._log.trace("_doPing - Can't send ping " + ping.id);
1101      return Promise.resolve();
1102    }
1103
1104    if (this._tooLateToSend) {
1105      // Too late to send now. Reject so we pend the ping to send it next time.
1106      this._log.trace("_doPing - Too late to send ping " + ping.id);
1107      Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE_TYPE").add("eTooLate");
1108      return Promise.reject();
1109    }
1110
1111    this._log.trace("_doPing - server: " + this._server + ", persisted: " + isPersisted +
1112                    ", id: " + id);
1113
1114    const url = this._buildSubmissionURL(ping);
1115
1116    // Don't send cookies with these requests.
1117    let request = new ServiceRequest({mozAnon: true});
1118    request.mozBackgroundRequest = true;
1119    request.timeout = Policy.pingSubmissionTimeout();
1120
1121    request.open("POST", url, true);
1122    request.overrideMimeType("text/plain");
1123    request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
1124    request.setRequestHeader("Date", Policy.now().toUTCString());
1125
1126    this._pendingPingRequests.set(id, request);
1127
1128    // Prevent the request channel from running though URLClassifier (bug 1296802)
1129    request.channel.loadFlags &= ~Ci.nsIChannel.LOAD_CLASSIFY_URI;
1130
1131    const monotonicStartTime = Utils.monotonicNow();
1132    let deferred = PromiseUtils.defer();
1133
1134    let onRequestFinished = (success, event) => {
1135      let onCompletion = () => {
1136        if (success) {
1137          let histogram = Telemetry.getHistogramById("TELEMETRY_SUCCESSFUL_SEND_PINGS_SIZE_KB");
1138          histogram.add(compressedPingSizeKB);
1139          deferred.resolve();
1140        } else {
1141          let histogram = Telemetry.getHistogramById("TELEMETRY_FAILED_SEND_PINGS_SIZE_KB");
1142          histogram.add(compressedPingSizeKB);
1143          deferred.reject(event);
1144        }
1145      };
1146
1147      this._pendingPingRequests.delete(id);
1148      this._onPingRequestFinished(success, monotonicStartTime, id, isPersisted)
1149        .then(() => onCompletion(),
1150              (error) => {
1151                this._log.error("_doPing - request success: " + success + ", error: " + error);
1152                onCompletion();
1153              });
1154    };
1155
1156    let errorhandler = (event) => {
1157      let failure = event.type;
1158      if (failure === "error") {
1159        failure = XHR_ERROR_TYPE[request.errorCode];
1160      }
1161
1162      TelemetryHealthPing.recordSendFailure(failure);
1163      Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE_TYPE").add(failure);
1164
1165      this._log.error("_doPing - error making request to " + url + ": " + failure);
1166      onRequestFinished(false, event);
1167    };
1168    request.onerror = errorhandler;
1169    request.ontimeout = errorhandler;
1170    request.onabort = errorhandler;
1171
1172    request.onload = (event) => {
1173      let status = request.status;
1174      let statusClass = status - (status % 100);
1175      let success = false;
1176
1177      if (statusClass === 200) {
1178        // We can treat all 2XX as success.
1179        this._log.info("_doPing - successfully loaded, status: " + status);
1180        success = true;
1181      } else if (statusClass === 400) {
1182        // 4XX means that something with the request was broken.
1183        this._log.error("_doPing - error submitting to " + url + ", status: " + status
1184                        + " - ping request broken?");
1185        Telemetry.getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS").add();
1186        // TODO: we should handle this better, but for now we should avoid resubmitting
1187        // broken requests by pretending success.
1188        success = true;
1189      } else if (statusClass === 500) {
1190        // 5XX means there was a server-side error and we should try again later.
1191        this._log.error("_doPing - error submitting to " + url + ", status: " + status
1192                        + " - server error, should retry later");
1193      } else {
1194        // We received an unexpected status code.
1195        this._log.error("_doPing - error submitting to " + url + ", status: " + status
1196                        + ", type: " + event.type);
1197      }
1198
1199      onRequestFinished(success, event);
1200    };
1201
1202    // If that's a legacy ping format, just send its payload.
1203    let networkPayload = isV4PingFormat(ping) ? ping : ping.payload;
1204    request.setRequestHeader("Content-Encoding", "gzip");
1205    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
1206                    .createInstance(Ci.nsIScriptableUnicodeConverter);
1207    converter.charset = "UTF-8";
1208    let startTime = Utils.monotonicNow();
1209    let utf8Payload = converter.ConvertFromUnicode(JSON.stringify(networkPayload));
1210    utf8Payload += converter.Finish();
1211    Telemetry.getHistogramById("TELEMETRY_STRINGIFY").add(Utils.monotonicNow() - startTime);
1212
1213    // Check the size and drop pings which are too big.
1214    const pingSizeBytes = utf8Payload.length;
1215    if (pingSizeBytes > TelemetryStorage.MAXIMUM_PING_SIZE) {
1216      this._log.error("_doPing - submitted ping exceeds the size limit, size: " + pingSizeBytes);
1217      Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND").add();
1218      Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB")
1219               .add(Math.floor(pingSizeBytes / 1024 / 1024));
1220      // We don't need to call |request.abort()| as it was not sent yet.
1221      this._pendingPingRequests.delete(id);
1222
1223      TelemetryHealthPing.recordDiscardedPing(ping.type);
1224      return TelemetryStorage.removePendingPing(id);
1225    }
1226
1227    let payloadStream = Cc["@mozilla.org/io/string-input-stream;1"]
1228                        .createInstance(Ci.nsIStringInputStream);
1229    startTime = Utils.monotonicNow();
1230    payloadStream.data = gzipCompressString(utf8Payload);
1231
1232    const compressedPingSizeKB = Math.floor(payloadStream.data.length / 1024);
1233    Telemetry.getHistogramById("TELEMETRY_COMPRESS").add(Utils.monotonicNow() - startTime);
1234    request.sendInputStream(payloadStream);
1235
1236    return deferred.promise;
1237  },
1238
1239  /**
1240   * Check if sending is temporarily disabled.
1241   * @return {Boolean} True if we can send pings to the server right now, false if
1242   *         sending is temporarily disabled.
1243   */
1244  get canSendNow() {
1245    // If the reporting policy was not accepted yet, don't send pings.
1246    if (!TelemetryReportingPolicy.canUpload()) {
1247      return false;
1248    }
1249
1250    return this._sendingEnabled;
1251  },
1252
1253  /**
1254   * Check if sending is disabled. If FHR is not allowed to upload,
1255   * pings are not sent to the server (Telemetry is a sub-feature of FHR). If trying
1256   * to send a deletion ping, don't block it.
1257   * If unified telemetry is off, don't send pings if Telemetry is disabled.
1258   *
1259   * @param {Object} [ping=null] A ping to be checked.
1260   * @return {Boolean} True if pings can be send to the servers, false otherwise.
1261   */
1262  sendingEnabled(ping = null) {
1263    // We only send pings from official builds, but allow overriding this for tests.
1264    if (!Telemetry.isOfficialTelemetry &&
1265        !this._testMode &&
1266        !this._overrideOfficialCheck) {
1267      return false;
1268    }
1269
1270    // With unified Telemetry, the FHR upload setting controls whether we can send pings.
1271    // The Telemetry pref enables sending extended data sets instead.
1272    if (IS_UNIFIED_TELEMETRY) {
1273      // Deletion pings are sent even if the upload is disabled.
1274      if (ping && isDeletionPing(ping)) {
1275        return true;
1276      }
1277      return Services.prefs.getBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, false);
1278    }
1279
1280    // Without unified Telemetry, the Telemetry enabled pref controls ping sending.
1281    return Utils.isTelemetryEnabled;
1282  },
1283
1284  /**
1285   * Track any pending ping send and save tasks through the promise passed here.
1286   * This is needed to block shutdown on any outstanding ping activity.
1287   */
1288  _trackPendingPingTask(promise) {
1289    let clear = () => this._pendingPingActivity.delete(promise);
1290    promise.then(clear, clear);
1291    this._pendingPingActivity.add(promise);
1292  },
1293
1294  /**
1295   * Return a promise that allows to wait on pending pings.
1296   * @return {Object<Promise>} A promise resolved when all the pending pings promises
1297   *         are resolved.
1298   */
1299  promisePendingPingActivity() {
1300    this._log.trace("promisePendingPingActivity - Waiting for ping task");
1301    let p = Array.from(this._pendingPingActivity, p => p.catch(ex => {
1302      this._log.error("promisePendingPingActivity - ping activity had an error", ex);
1303    }));
1304    p.push(SendScheduler.waitOnSendTask());
1305    return Promise.all(p);
1306  },
1307
1308  async _persistCurrentPings() {
1309    for (let [id, ping] of this._currentPings) {
1310      try {
1311        await savePing(ping);
1312        this._log.trace("_persistCurrentPings - saved ping " + id);
1313      } catch (ex) {
1314        this._log.error("_persistCurrentPings - failed to save ping " + id, ex);
1315      } finally {
1316        this._currentPings.delete(id);
1317      }
1318    }
1319  },
1320
1321  /**
1322   * Returns the current pending, not yet persisted, pings, newest first.
1323   */
1324  getUnpersistedPings() {
1325    let current = [...this._currentPings.values()];
1326    current.reverse();
1327    return current;
1328  },
1329
1330  getShutdownState() {
1331    return {
1332      sendingEnabled: this._sendingEnabled,
1333      pendingPingRequestCount: this._pendingPingRequests.size,
1334      pendingPingActivityCount: this._pendingPingActivity.size,
1335      unpersistedPingCount: this._currentPings.size,
1336      persistedPingCount: TelemetryStorage.getPendingPingList().length,
1337      schedulerState: SendScheduler.getShutdownState(),
1338    };
1339  },
1340
1341  runPingSender(url, pingPath) {
1342    if (AppConstants.platform === "android") {
1343      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
1344    }
1345
1346    const exeName = AppConstants.platform === "win" ? "pingsender.exe"
1347                                                    : "pingsender";
1348
1349    let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
1350    exe.append(exeName);
1351
1352    let process = Cc["@mozilla.org/process/util;1"]
1353                  .createInstance(Ci.nsIProcess);
1354    process.init(exe);
1355    process.startHidden = true;
1356    process.noShell = true;
1357    process.run(/* blocking */ false, [url, pingPath], 2);
1358  },
1359};
1360