Best Python code snippet using localstack_python
TelemetryStorage.jsm
Source:TelemetryStorage.jsm
1/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */2/* This Source Code Form is subject to the terms of the Mozilla Public3 * License, v. 2.0. If a copy of the MPL was not distributed with this4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */5"use strict";6this.EXPORTED_SYMBOLS = ["TelemetryStorage"];7const Cc = Components.classes;8const Ci = Components.interfaces;9const Cr = Components.results;10const Cu = Components.utils;11Cu.import("resource://gre/modules/AppConstants.jsm", this);12Cu.import("resource://gre/modules/Log.jsm");13Cu.import("resource://gre/modules/Services.jsm", this);14Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);15Cu.import("resource://gre/modules/osfile.jsm", this);16Cu.import("resource://gre/modules/Task.jsm", this);17Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);18Cu.import("resource://gre/modules/Promise.jsm", this);19Cu.import("resource://gre/modules/Preferences.jsm", this);20const LOGGER_NAME = "Toolkit.Telemetry";21const LOGGER_PREFIX = "TelemetryStorage::";22const Telemetry = Services.telemetry;23const Utils = TelemetryUtils;24// Compute the path of the pings archive on the first use.25const DATAREPORTING_DIR = "datareporting";26const PINGS_ARCHIVE_DIR = "archived";27const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";28const DELETION_PING_FILE_NAME = "pending-deletion-ping";29const SESSION_STATE_FILE_NAME = "session-state.json";30XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() {31 return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);32});33XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {34 return OS.Path.join(gDataReportingDir, PINGS_ARCHIVE_DIR);35});36XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() {37 return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME);38});39XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() {40 return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME);41});42XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",43 "resource://services-common/utils.js");44// Maxmimum time, in milliseconds, archive pings should be retained.45const MAX_ARCHIVED_PINGS_RETENTION_MS = 60 * 24 * 60 * 60 * 1000; // 60 days46// Maximum space the archive can take on disk (in Bytes).47const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB48// Maximum space the outgoing pings can take on disk, for Desktop (in Bytes).49const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB50// Maximum space the outgoing pings can take on disk, for Mobile (in Bytes).51const PENDING_PINGS_QUOTA_BYTES_MOBILE = 1024 * 1024; // 1 MB52// The maximum size a pending/archived ping can take on disk.53const PING_FILE_MAXIMUM_SIZE_BYTES = 1024 * 1024; // 1 MB54// This special value is submitted when the archive is outside of the quota.55const ARCHIVE_SIZE_PROBE_SPECIAL_VALUE = 300;56// This special value is submitted when the pending pings is outside of the quota, as57// we don't know the size of the pings above the quota.58const PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE = 17;59const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;60/**61 * This is thrown by |TelemetryStorage.loadPingFile| when reading the ping62 * from the disk fails.63 */64function PingReadError(message="Error reading the ping file", becauseNoSuchFile = false) {65 Error.call(this, message);66 let error = new Error();67 this.name = "PingReadError";68 this.message = message;69 this.stack = error.stack;70 this.becauseNoSuchFile = becauseNoSuchFile;71}72PingReadError.prototype = Object.create(Error.prototype);73PingReadError.prototype.constructor = PingReadError;74/**75 * This is thrown by |TelemetryStorage.loadPingFile| when parsing the ping JSON76 * content fails.77 */78function PingParseError(message="Error parsing ping content") {79 Error.call(this, message);80 let error = new Error();81 this.name = "PingParseError";82 this.message = message;83 this.stack = error.stack;84}85PingParseError.prototype = Object.create(Error.prototype);86PingParseError.prototype.constructor = PingParseError;87/**88 * This is a policy object used to override behavior for testing.89 */90var Policy = {91 now: () => new Date(),92 getArchiveQuota: () => ARCHIVE_QUOTA_BYTES,93 getPendingPingsQuota: () => (AppConstants.platform in ["android", "gonk"])94 ? PENDING_PINGS_QUOTA_BYTES_MOBILE95 : PENDING_PINGS_QUOTA_BYTES_DESKTOP,96};97/**98 * Wait for all promises in iterable to resolve or reject. This function99 * always resolves its promise with undefined, and never rejects.100 */101function waitForAll(it) {102 let dummy = () => {};103 let promises = Array.from(it, p => p.catch(dummy));104 return Promise.all(promises);105}106/**107 * Permanently intern the given string. This is mainly used for the ping.type108 * strings that can be excessively duplicated in the _archivedPings map. Do not109 * pass large or temporary strings to this function.110 */111function internString(str) {112 return Symbol.keyFor(Symbol.for(str));113}114this.TelemetryStorage = {115 get pingDirectoryPath() {116 return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");117 },118 /**119 * The maximum size a ping can have, in bytes.120 */121 get MAXIMUM_PING_SIZE() {122 return PING_FILE_MAXIMUM_SIZE_BYTES;123 },124 /**125 * Shutdown & block on any outstanding async activity in this module.126 *127 * @return {Promise} Promise that is resolved when shutdown is complete.128 */129 shutdown: function() {130 return TelemetryStorageImpl.shutdown();131 },132 /**133 * Save an archived ping to disk.134 *135 * @param {object} ping The ping data to archive.136 * @return {promise} Promise that is resolved when the ping is successfully archived.137 */138 saveArchivedPing: function(ping) {139 return TelemetryStorageImpl.saveArchivedPing(ping);140 },141 /**142 * Load an archived ping from disk.143 *144 * @param {string} id The pings id.145 * @return {promise<object>} Promise that is resolved with the ping data.146 */147 loadArchivedPing: function(id) {148 return TelemetryStorageImpl.loadArchivedPing(id);149 },150 /**151 * Get a list of info on the archived pings.152 * This will scan the archive directory and grab basic data about the existing153 * pings out of their filename.154 *155 * @return {promise<sequence<object>>}156 */157 loadArchivedPingList: function() {158 return TelemetryStorageImpl.loadArchivedPingList();159 },160 /**161 * Clean the pings archive by removing old pings.162 * This will scan the archive directory.163 *164 * @return {Promise} Resolved when the cleanup task completes.165 */166 runCleanPingArchiveTask: function() {167 return TelemetryStorageImpl.runCleanPingArchiveTask();168 },169 /**170 * Run the task to enforce the pending pings quota.171 *172 * @return {Promise} Resolved when the cleanup task completes.173 */174 runEnforcePendingPingsQuotaTask: function() {175 return TelemetryStorageImpl.runEnforcePendingPingsQuotaTask();176 },177 /**178 * Run the task to remove all the pending pings (except the deletion ping).179 *180 * @return {Promise} Resolved when the pings are removed.181 */182 runRemovePendingPingsTask: function() {183 return TelemetryStorageImpl.runRemovePendingPingsTask();184 },185 /**186 * Reset the storage state in tests.187 */188 reset: function() {189 return TelemetryStorageImpl.reset();190 },191 /**192 * Test method that allows waiting on the archive clean task to finish.193 */194 testCleanupTaskPromise: function() {195 return (TelemetryStorageImpl._cleanArchiveTask || Promise.resolve());196 },197 /**198 * Test method that allows waiting on the pending pings quota task to finish.199 */200 testPendingQuotaTaskPromise: function() {201 return (TelemetryStorageImpl._enforcePendingPingsQuotaTask || Promise.resolve());202 },203 /**204 * Save a pending - outgoing - ping to disk and track it.205 *206 * @param {Object} ping The ping data.207 * @return {Promise} Resolved when the ping was saved.208 */209 savePendingPing: function(ping) {210 return TelemetryStorageImpl.savePendingPing(ping);211 },212 /**213 * Saves session data to disk.214 * @param {Object} sessionData The session data.215 * @return {Promise} Resolved when the data was saved.216 */217 saveSessionData: function(sessionData) {218 return TelemetryStorageImpl.saveSessionData(sessionData);219 },220 /**221 * Loads session data from a session data file.222 * @return {Promise<object>} Resolved with the session data in object form.223 */224 loadSessionData: function() {225 return TelemetryStorageImpl.loadSessionData();226 },227 /**228 * Load a pending ping from disk by id.229 *230 * @param {String} id The pings id.231 * @return {Promise} Resolved with the loaded ping data.232 */233 loadPendingPing: function(id) {234 return TelemetryStorageImpl.loadPendingPing(id);235 },236 /**237 * Remove a pending ping from disk by id.238 *239 * @param {String} id The pings id.240 * @return {Promise} Resolved when the ping was removed.241 */242 removePendingPing: function(id) {243 return TelemetryStorageImpl.removePendingPing(id);244 },245 /**246 * Returns a list of the currently pending pings in the format:247 * {248 * id: <string>, // The pings UUID.249 * lastModificationDate: <number>, // Timestamp of the pings last modification.250 * }251 * This populates the list by scanning the disk.252 *253 * @return {Promise<sequence>} Resolved with the ping list.254 */255 loadPendingPingList: function() {256 return TelemetryStorageImpl.loadPendingPingList();257 },258 /**259 * Returns a list of the currently pending pings in the format:260 * {261 * id: <string>, // The pings UUID.262 * lastModificationDate: <number>, // Timestamp of the pings last modification.263 * }264 * This does not scan pending pings on disk.265 *266 * @return {sequence} The current pending ping list.267 */268 getPendingPingList: function() {269 return TelemetryStorageImpl.getPendingPingList();270 },271 /**272 * Save an aborted-session ping to disk. This goes to a special location so273 * it is not picked up as a pending ping.274 *275 * @param {object} ping The ping data to save.276 * @return {promise} Promise that is resolved when the ping is successfully saved.277 */278 saveAbortedSessionPing: function(ping) {279 return TelemetryStorageImpl.saveAbortedSessionPing(ping);280 },281 /**282 * Load the aborted-session ping from disk if present.283 *284 * @return {promise<object>} Promise that is resolved with the ping data if found.285 * Otherwise returns null.286 */287 loadAbortedSessionPing: function() {288 return TelemetryStorageImpl.loadAbortedSessionPing();289 },290 /**291 * Save the deletion ping.292 * @param ping The deletion ping.293 * @return {Promise} A promise resolved when the ping is saved.294 */295 saveDeletionPing: function(ping) {296 return TelemetryStorageImpl.saveDeletionPing(ping);297 },298 /**299 * Remove the deletion ping.300 * @return {Promise} Resolved when the ping is deleted from the disk.301 */302 removeDeletionPing: function() {303 return TelemetryStorageImpl.removeDeletionPing();304 },305 /**306 * Check if the ping id identifies a deletion ping.307 */308 isDeletionPing: function(aPingId) {309 return TelemetryStorageImpl.isDeletionPing(aPingId);310 },311 /**312 * Remove the aborted-session ping if present.313 *314 * @return {promise} Promise that is resolved once the ping is removed.315 */316 removeAbortedSessionPing: function() {317 return TelemetryStorageImpl.removeAbortedSessionPing();318 },319 /**320 * Save a single ping to a file.321 *322 * @param {object} ping The content of the ping to save.323 * @param {string} file The destination file.324 * @param {bool} overwrite If |true|, the file will be overwritten if it exists,325 * if |false| the file will not be overwritten and no error will be reported if326 * the file exists.327 * @returns {promise}328 */329 savePingToFile: function(ping, file, overwrite) {330 return TelemetryStorageImpl.savePingToFile(ping, file, overwrite);331 },332 /**333 * Save a ping to its file.334 *335 * @param {object} ping The content of the ping to save.336 * @param {bool} overwrite If |true|, the file will be overwritten337 * if it exists.338 * @returns {promise}339 */340 savePing: function(ping, overwrite) {341 return TelemetryStorageImpl.savePing(ping, overwrite);342 },343 /**344 * Add a ping to the saved pings directory so that it gets saved345 * and sent along with other pings.346 *347 * @param {Object} pingData The ping object.348 * @return {Promise} A promise resolved when the ping is saved to the pings directory.349 */350 addPendingPing: function(pingData) {351 return TelemetryStorageImpl.addPendingPing(pingData);352 },353 /**354 * Remove the file for a ping355 *356 * @param {object} ping The ping.357 * @returns {promise}358 */359 cleanupPingFile: function(ping) {360 return TelemetryStorageImpl.cleanupPingFile(ping);361 },362 /**363 * The number of pending pings on disk.364 */365 get pendingPingCount() {366 return TelemetryStorageImpl.pendingPingCount;367 },368 /**369 * Loads a ping file.370 * @param {String} aFilePath The path of the ping file.371 * @return {Promise<Object>} A promise resolved with the ping content or rejected if the372 * ping contains invalid data.373 */374 loadPingFile: Task.async(function* (aFilePath) {375 return TelemetryStorageImpl.loadPingFile(aFilePath);376 }),377 /**378 * Remove FHR database files. This is temporary and will be dropped in379 * the future.380 * @return {Promise} Resolved when the database files are deleted.381 */382 removeFHRDatabase: function() {383 return TelemetryStorageImpl.removeFHRDatabase();384 },385 /**386 * Only used in tests, builds an archived ping path from the ping metadata.387 * @param {String} aPingId The ping id.388 * @param {Object} aDate The ping creation date.389 * @param {String} aType The ping type.390 * @return {String} The full path to the archived ping.391 */392 _testGetArchivedPingPath: function(aPingId, aDate, aType) {393 return getArchivedPingPath(aPingId, aDate, aType);394 },395 /**396 * Only used in tests, this helper extracts ping metadata from a given filename.397 *398 * @param fileName {String} The filename.399 * @return {Object} Null if the filename didn't match the expected form.400 * Otherwise an object with the extracted data in the form:401 * { timestamp: <number>,402 * id: <string>,403 * type: <string> }404 */405 _testGetArchivedPingDataFromFileName: function(aFileName) {406 return TelemetryStorageImpl._getArchivedPingDataFromFileName(aFileName);407 },408 /**409 * Only used in tests, this helper allows cleaning up the pending ping storage.410 */411 testClearPendingPings: function() {412 return TelemetryStorageImpl.runRemovePendingPingsTask();413 }414};415/**416 * This object allows the serialisation of asynchronous tasks. This is particularly417 * useful to serialise write access to the disk in order to prevent race conditions418 * to corrupt the data being written.419 * We are using this to synchronize saving to the file that TelemetrySession persists420 * its state in.421 */422function SaveSerializer() {423 this._queuedOperations = [];424 this._queuedInProgress = false;425 this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);426}427SaveSerializer.prototype = {428 /**429 * Enqueues an operation to a list to serialise their execution in order to prevent race430 * conditions. Useful to serialise access to disk.431 *432 * @param {Function} aFunction The task function to enqueue. It must return a promise.433 * @return {Promise} A promise resolved when the enqueued task completes.434 */435 enqueueTask: function (aFunction) {436 let promise = new Promise((resolve, reject) =>437 this._queuedOperations.push([aFunction, resolve, reject]));438 if (this._queuedOperations.length == 1) {439 this._popAndPerformQueuedOperation();440 }441 return promise;442 },443 /**444 * Make sure to flush all the pending operations.445 * @return {Promise} A promise resolved when all the pending operations have completed.446 */447 flushTasks: function () {448 let dummyTask = () => new Promise(resolve => resolve());449 return this.enqueueTask(dummyTask);450 },451 /**452 * Pop a task from the queue, executes it and continue to the next one.453 * This function recursively pops all the tasks.454 */455 _popAndPerformQueuedOperation: function () {456 if (!this._queuedOperations.length || this._queuedInProgress) {457 return;458 }459 this._log.trace("_popAndPerformQueuedOperation - Performing queued operation.");460 let [func, resolve, reject] = this._queuedOperations.shift();461 let promise;462 try {463 this._queuedInProgress = true;464 promise = func();465 } catch (ex) {466 this._log.warn("_popAndPerformQueuedOperation - Queued operation threw during execution. ",467 ex);468 this._queuedInProgress = false;469 reject(ex);470 this._popAndPerformQueuedOperation();471 return;472 }473 if (!promise || typeof(promise.then) != "function") {474 let msg = "Queued operation did not return a promise: " + func;475 this._log.warn("_popAndPerformQueuedOperation - " + msg);476 this._queuedInProgress = false;477 reject(new Error(msg));478 this._popAndPerformQueuedOperation();479 return;480 }481 promise.then(result => {482 this._queuedInProgress = false;483 resolve(result);484 this._popAndPerformQueuedOperation();485 },486 error => {487 this._log.warn("_popAndPerformQueuedOperation - Failure when performing queued operation.",488 error);489 this._queuedInProgress = false;490 reject(error);491 this._popAndPerformQueuedOperation();492 });493 },494};495var TelemetryStorageImpl = {496 _logger: null,497 // Used to serialize aborted session ping writes to disk.498 _abortedSessionSerializer: new SaveSerializer(),499 // Used to serialize deletion ping writes to disk.500 _deletionPingSerializer: new SaveSerializer(),501 // Used to serialize session state writes to disk.502 _stateSaveSerializer: new SaveSerializer(),503 // Tracks the archived pings in a Map of (id -> {timestampCreated, type}).504 // We use this to cache info on archived pings to avoid scanning the disk more than once.505 _archivedPings: new Map(),506 // A set of promises for pings currently being archived507 _activelyArchiving: new Set(),508 // Track the archive loading task to prevent multiple tasks from being executed.509 _scanArchiveTask: null,510 // Track the archive cleanup task.511 _cleanArchiveTask: null,512 // Whether we already scanned the archived pings on disk.513 _scannedArchiveDirectory: false,514 // Track the pending ping removal task.515 _removePendingPingsTask: null,516 // This tracks all the pending async ping save activity.517 _activePendingPingSaves: new Set(),518 // Tracks the pending pings in a Map of (id -> {timestampCreated, type}).519 // We use this to cache info on pending pings to avoid scanning the disk more than once.520 _pendingPings: new Map(),521 // Track the pending pings enforce quota task.522 _enforcePendingPingsQuotaTask: null,523 // Track the shutdown process to bail out of the clean up task quickly.524 _shutdown: false,525 get _log() {526 if (!this._logger) {527 this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);528 }529 return this._logger;530 },531 /**532 * Shutdown & block on any outstanding async activity in this module.533 *534 * @return {Promise} Promise that is resolved when shutdown is complete.535 */536 shutdown: Task.async(function*() {537 this._shutdown = true;538 // If the following tasks are still running, block on them. They will bail out as soon539 // as possible.540 yield this._abortedSessionSerializer.flushTasks().catch(ex => {541 this._log.error("shutdown - failed to flush aborted-session writes", ex);542 });543 yield this._deletionPingSerializer.flushTasks().catch(ex => {544 this._log.error("shutdown - failed to flush deletion ping writes", ex);545 });546 if (this._cleanArchiveTask) {547 yield this._cleanArchiveTask.catch(ex => {548 this._log.error("shutdown - the archive cleaning task failed", ex);549 });550 }551 if (this._enforcePendingPingsQuotaTask) {552 yield this._enforcePendingPingsQuotaTask.catch(ex => {553 this._log.error("shutdown - the pending pings quota task failed", ex);554 });555 }556 if (this._removePendingPingsTask) {557 yield this._removePendingPingsTask.catch(ex => {558 this._log.error("shutdown - the pending pings removal task failed", ex);559 });560 }561 // Wait on pending pings still being saved. While OS.File should have shutdown562 // blockers in place, we a) have seen weird errors being reported that might563 // indicate a bad shutdown path and b) might have completion handlers hanging564 // off the save operations that don't expect to be late in shutdown.565 yield this.promisePendingPingSaves();566 }),567 /**568 * Save an archived ping to disk.569 *570 * @param {object} ping The ping data to archive.571 * @return {promise} Promise that is resolved when the ping is successfully archived.572 */573 saveArchivedPing: function(ping) {574 let promise = this._saveArchivedPingTask(ping);575 this._activelyArchiving.add(promise);576 promise.then((r) => { this._activelyArchiving.delete(promise); },577 (e) => { this._activelyArchiving.delete(promise); });578 return promise;579 },580 _saveArchivedPingTask: Task.async(function*(ping) {581 const creationDate = new Date(ping.creationDate);582 if (this._archivedPings.has(ping.id)) {583 const data = this._archivedPings.get(ping.id);584 if (data.timestampCreated > creationDate.getTime()) {585 this._log.error("saveArchivedPing - trying to overwrite newer ping with the same id");586 return Promise.reject(new Error("trying to overwrite newer ping with the same id"));587 }588 this._log.warn("saveArchivedPing - overwriting older ping with the same id");589 }590 // Get the archived ping path and append the lz4 suffix to it (so we have 'jsonlz4').591 const filePath = getArchivedPingPath(ping.id, creationDate, ping.type) + "lz4";592 yield OS.File.makeDir(OS.Path.dirname(filePath), { ignoreExisting: true,593 from: OS.Constants.Path.profileDir });594 yield this.savePingToFile(ping, filePath, /* overwrite*/ true, /* compressed*/ true);595 this._archivedPings.set(ping.id, {596 timestampCreated: creationDate.getTime(),597 type: internString(ping.type),598 });599 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").add();600 return undefined;601 }),602 /**603 * Load an archived ping from disk.604 *605 * @param {string} id The pings id.606 * @return {promise<object>} Promise that is resolved with the ping data.607 */608 loadArchivedPing: Task.async(function*(id) {609 this._log.trace("loadArchivedPing - id: " + id);610 const data = this._archivedPings.get(id);611 if (!data) {612 this._log.trace("loadArchivedPing - no ping with id: " + id);613 return Promise.reject(new Error("TelemetryStorage.loadArchivedPing - no ping with id " + id));614 }615 const path = getArchivedPingPath(id, new Date(data.timestampCreated), data.type);616 const pathCompressed = path + "lz4";617 // Purge pings which are too big.618 let checkSize = function*(path) {619 const fileSize = (yield OS.File.stat(path)).size;620 if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {621 Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB")622 .add(Math.floor(fileSize / 1024 / 1024));623 Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").add();624 yield OS.File.remove(path, {ignoreAbsent: true});625 throw new Error("loadArchivedPing - exceeded the maximum ping size: " + fileSize);626 }627 };628 try {629 // Try to load a compressed version of the archived ping first.630 this._log.trace("loadArchivedPing - loading ping from: " + pathCompressed);631 yield* checkSize(pathCompressed);632 return yield this.loadPingFile(pathCompressed, /* compressed*/ true);633 } catch (ex) {634 if (!ex.becauseNoSuchFile) {635 throw ex;636 }637 // If that fails, look for the uncompressed version.638 this._log.trace("loadArchivedPing - compressed ping not found, loading: " + path);639 yield* checkSize(path);640 return yield this.loadPingFile(path, /* compressed*/ false);641 }642 }),643 /**644 * Saves session data to disk.645 */646 saveSessionData: function(sessionData) {647 return this._stateSaveSerializer.enqueueTask(() => this._saveSessionData(sessionData));648 },649 _saveSessionData: Task.async(function* (sessionData) {650 let dataDir = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);651 yield OS.File.makeDir(dataDir);652 let filePath = OS.Path.join(gDataReportingDir, SESSION_STATE_FILE_NAME);653 try {654 yield CommonUtils.writeJSON(sessionData, filePath);655 } catch (e) {656 this._log.error("_saveSessionData - Failed to write session data to " + filePath, e);657 Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_SAVE").add(1);658 }659 }),660 /**661 * Loads session data from the session data file.662 * @return {Promise<Object>} A promise resolved with an object on success,663 * with null otherwise.664 */665 loadSessionData: function() {666 return this._stateSaveSerializer.enqueueTask(() => this._loadSessionData());667 },668 _loadSessionData: Task.async(function* () {669 const dataFile = OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR,670 SESSION_STATE_FILE_NAME);671 let content;672 try {673 content = yield OS.File.read(dataFile, { encoding: "utf-8" });674 } catch (ex) {675 this._log.info("_loadSessionData - can not load session data file", ex);676 Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_LOAD").add(1);677 return null;678 }679 let data;680 try {681 data = JSON.parse(content);682 } catch (ex) {683 this._log.error("_loadSessionData - failed to parse session data", ex);684 Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_PARSE").add(1);685 return null;686 }687 return data;688 }),689 /**690 * Remove an archived ping from disk.691 *692 * @param {string} id The pings id.693 * @param {number} timestampCreated The pings creation timestamp.694 * @param {string} type The pings type.695 * @return {promise<object>} Promise that is resolved when the pings is removed.696 */697 _removeArchivedPing: Task.async(function*(id, timestampCreated, type) {698 this._log.trace("_removeArchivedPing - id: " + id + ", timestampCreated: " + timestampCreated + ", type: " + type);699 const path = getArchivedPingPath(id, new Date(timestampCreated), type);700 const pathCompressed = path + "lz4";701 this._log.trace("_removeArchivedPing - removing ping from: " + path);702 yield OS.File.remove(path, {ignoreAbsent: true});703 yield OS.File.remove(pathCompressed, {ignoreAbsent: true});704 // Remove the ping from the cache.705 this._archivedPings.delete(id);706 }),707 /**708 * Clean the pings archive by removing old pings.709 *710 * @return {Promise} Resolved when the cleanup task completes.711 */712 runCleanPingArchiveTask: function() {713 // If there's an archive cleaning task already running, return it.714 if (this._cleanArchiveTask) {715 return this._cleanArchiveTask;716 }717 // Make sure to clear |_cleanArchiveTask| once done.718 let clear = () => this._cleanArchiveTask = null;719 // Since there's no archive cleaning task running, start it.720 this._cleanArchiveTask = this._cleanArchive().then(clear, clear);721 return this._cleanArchiveTask;722 },723 /**724 * Removes pings which are too old from the pings archive.725 * @return {Promise} Resolved when the ping age check is complete.726 */727 _purgeOldPings: Task.async(function*() {728 this._log.trace("_purgeOldPings");729 const nowDate = Policy.now();730 const startTimeStamp = nowDate.getTime();731 let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath);732 let subdirs = (yield dirIterator.nextBatch()).filter(e => e.isDir);733 dirIterator.close();734 // Keep track of the newest removed month to update the cache, if needed.735 let newestRemovedMonthTimestamp = null;736 let evictedDirsCount = 0;737 let maxDirAgeInMonths = 0;738 // Walk through the monthly subdirs of the form <YYYY-MM>/739 for (let dir of subdirs) {740 if (this._shutdown) {741 this._log.trace("_purgeOldPings - Terminating the clean up task due to shutdown");742 return;743 }744 if (!isValidArchiveDir(dir.name)) {745 this._log.warn("_purgeOldPings - skipping invalidly named subdirectory " + dir.path);746 continue;747 }748 const archiveDate = getDateFromArchiveDir(dir.name);749 if (!archiveDate) {750 this._log.warn("_purgeOldPings - skipping invalid subdirectory date " + dir.path);751 continue;752 }753 // If this archive directory is older than 180 days, remove it.754 if ((startTimeStamp - archiveDate.getTime()) > MAX_ARCHIVED_PINGS_RETENTION_MS) {755 try {756 yield OS.File.removeDir(dir.path);757 evictedDirsCount++;758 // Update the newest removed month.759 newestRemovedMonthTimestamp = Math.max(archiveDate, newestRemovedMonthTimestamp);760 } catch (ex) {761 this._log.error("_purgeOldPings - Unable to remove " + dir.path, ex);762 }763 } else {764 // We're not removing this directory, so record the age for the oldest directory.765 const dirAgeInMonths = Utils.getElapsedTimeInMonths(archiveDate, nowDate);766 maxDirAgeInMonths = Math.max(dirAgeInMonths, maxDirAgeInMonths);767 }768 }769 // Trigger scanning of the archived pings.770 yield this.loadArchivedPingList();771 // Refresh the cache: we could still skip this, but it's cheap enough to keep it772 // to avoid introducing task dependencies.773 if (newestRemovedMonthTimestamp) {774 // Scan the archive cache for pings older than the newest directory pruned above.775 for (let [id, info] of this._archivedPings) {776 const timestampCreated = new Date(info.timestampCreated);777 if (timestampCreated.getTime() > newestRemovedMonthTimestamp) {778 continue;779 }780 // Remove outdated pings from the cache.781 this._archivedPings.delete(id);782 }783 }784 const endTimeStamp = Policy.now().getTime();785 // Save the time it takes to evict old directories and the eviction count.786 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS")787 .add(evictedDirsCount);788 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_DIRS_MS")789 .add(Math.ceil(endTimeStamp - startTimeStamp));790 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE")791 .add(maxDirAgeInMonths);792 }),793 /**794 * Enforce a disk quota for the pings archive.795 * @return {Promise} Resolved when the quota check is complete.796 */797 _enforceArchiveQuota: Task.async(function*() {798 this._log.trace("_enforceArchiveQuota");799 let startTimeStamp = Policy.now().getTime();800 // Build an ordered list, from newer to older, of archived pings.801 let pingList = Array.from(this._archivedPings, p => ({802 id: p[0],803 timestampCreated: p[1].timestampCreated,804 type: p[1].type,805 }));806 pingList.sort((a, b) => b.timestampCreated - a.timestampCreated);807 // If our archive is too big, we should reduce it to reach 90% of the quota.808 const SAFE_QUOTA = Policy.getArchiveQuota() * 0.9;809 // The index of the last ping to keep. Pings older than this one will be deleted if810 // the archive exceeds the quota.811 let lastPingIndexToKeep = null;812 let archiveSizeInBytes = 0;813 // Find the disk size of the archive.814 for (let i = 0; i < pingList.length; i++) {815 if (this._shutdown) {816 this._log.trace("_enforceArchiveQuota - Terminating the clean up task due to shutdown");817 return;818 }819 let ping = pingList[i];820 // Get the size for this ping.821 const fileSize =822 yield getArchivedPingSize(ping.id, new Date(ping.timestampCreated), ping.type);823 if (!fileSize) {824 this._log.warn("_enforceArchiveQuota - Unable to find the size of ping " + ping.id);825 continue;826 }827 // Enforce a maximum file size limit on archived pings.828 if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {829 this._log.error("_enforceArchiveQuota - removing file exceeding size limit, size: " + fileSize);830 // We just remove the ping from the disk, we don't bother removing it from pingList831 // since it won't contribute to the quota.832 yield this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type)833 .catch(e => this._log.error("_enforceArchiveQuota - failed to remove archived ping" + ping.id));834 Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB")835 .add(Math.floor(fileSize / 1024 / 1024));836 Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").add();837 continue;838 }839 archiveSizeInBytes += fileSize;840 if (archiveSizeInBytes < SAFE_QUOTA) {841 // We save the index of the last ping which is ok to keep in order to speed up ping842 // pruning.843 lastPingIndexToKeep = i;844 } else if (archiveSizeInBytes > Policy.getArchiveQuota()) {845 // Ouch, our ping archive is too big. Bail out and start pruning!846 break;847 }848 }849 // Save the time it takes to check if the archive is over-quota.850 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS")851 .add(Math.round(Policy.now().getTime() - startTimeStamp));852 let submitProbes = (sizeInMB, evictedPings, elapsedMs) => {853 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").add(sizeInMB);854 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").add(evictedPings);855 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").add(elapsedMs);856 };857 // Check if we're using too much space. If not, submit the archive size and bail out.858 if (archiveSizeInBytes < Policy.getArchiveQuota()) {859 submitProbes(Math.round(archiveSizeInBytes / 1024 / 1024), 0, 0);860 return;861 }862 this._log.info("_enforceArchiveQuota - archive size: " + archiveSizeInBytes + "bytes"863 + ", safety quota: " + SAFE_QUOTA + "bytes");864 startTimeStamp = Policy.now().getTime();865 let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1);866 // Remove all the pings older than the last one which we are safe to keep.867 for (let ping of pingsToPurge) {868 if (this._shutdown) {869 this._log.trace("_enforceArchiveQuota - Terminating the clean up task due to shutdown");870 return;871 }872 // This list is guaranteed to be in order, so remove the pings at its873 // beginning (oldest).874 yield this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type);875 }876 const endTimeStamp = Policy.now().getTime();877 submitProbes(ARCHIVE_SIZE_PROBE_SPECIAL_VALUE, pingsToPurge.length,878 Math.ceil(endTimeStamp - startTimeStamp));879 }),880 _cleanArchive: Task.async(function*() {881 this._log.trace("cleanArchiveTask");882 if (!(yield OS.File.exists(gPingsArchivePath))) {883 return;884 }885 // Remove pings older than 180 days.886 try {887 yield this._purgeOldPings();888 } catch (ex) {889 this._log.error("_cleanArchive - There was an error removing old directories", ex);890 }891 // Make sure we respect the archive disk quota.892 yield this._enforceArchiveQuota();893 }),894 /**895 * Run the task to enforce the pending pings quota.896 *897 * @return {Promise} Resolved when the cleanup task completes.898 */899 runEnforcePendingPingsQuotaTask: Task.async(function*() {900 // If there's a cleaning task already running, return it.901 if (this._enforcePendingPingsQuotaTask) {902 return this._enforcePendingPingsQuotaTask;903 }904 // Since there's no quota enforcing task running, start it.905 try {906 this._enforcePendingPingsQuotaTask = this._enforcePendingPingsQuota();907 yield this._enforcePendingPingsQuotaTask;908 } finally {909 this._enforcePendingPingsQuotaTask = null;910 }911 return undefined;912 }),913 /**914 * Enforce a disk quota for the pending pings.915 * @return {Promise} Resolved when the quota check is complete.916 */917 _enforcePendingPingsQuota: Task.async(function*() {918 this._log.trace("_enforcePendingPingsQuota");919 let startTimeStamp = Policy.now().getTime();920 // Build an ordered list, from newer to older, of pending pings.921 let pingList = Array.from(this._pendingPings, p => ({922 id: p[0],923 lastModificationDate: p[1].lastModificationDate,924 }));925 pingList.sort((a, b) => b.lastModificationDate - a.lastModificationDate);926 // If our pending pings directory is too big, we should reduce it to reach 90% of the quota.927 const SAFE_QUOTA = Policy.getPendingPingsQuota() * 0.9;928 // The index of the last ping to keep. Pings older than this one will be deleted if929 // the pending pings directory size exceeds the quota.930 let lastPingIndexToKeep = null;931 let pendingPingsSizeInBytes = 0;932 // Find the disk size of the pending pings directory.933 for (let i = 0; i < pingList.length; i++) {934 if (this._shutdown) {935 this._log.trace("_enforcePendingPingsQuota - Terminating the clean up task due to shutdown");936 return;937 }938 let ping = pingList[i];939 // Get the size for this ping.940 const fileSize = yield getPendingPingSize(ping.id);941 if (!fileSize) {942 this._log.warn("_enforcePendingPingsQuota - Unable to find the size of ping " + ping.id);943 continue;944 }945 pendingPingsSizeInBytes += fileSize;946 if (pendingPingsSizeInBytes < SAFE_QUOTA) {947 // We save the index of the last ping which is ok to keep in order to speed up ping948 // pruning.949 lastPingIndexToKeep = i;950 } else if (pendingPingsSizeInBytes > Policy.getPendingPingsQuota()) {951 // Ouch, our pending pings directory size is too big. Bail out and start pruning!952 break;953 }954 }955 // Save the time it takes to check if the pending pings are over-quota.956 Telemetry.getHistogramById("TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS")957 .add(Math.round(Policy.now().getTime() - startTimeStamp));958 let recordHistograms = (sizeInMB, evictedPings, elapsedMs) => {959 Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").add(sizeInMB);960 Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").add(evictedPings);961 Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").add(elapsedMs);962 };963 // Check if we're using too much space. If not, bail out.964 if (pendingPingsSizeInBytes < Policy.getPendingPingsQuota()) {965 recordHistograms(Math.round(pendingPingsSizeInBytes / 1024 / 1024), 0, 0);966 return;967 }968 this._log.info("_enforcePendingPingsQuota - size: " + pendingPingsSizeInBytes + "bytes"969 + ", safety quota: " + SAFE_QUOTA + "bytes");970 startTimeStamp = Policy.now().getTime();971 let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1);972 // Remove all the pings older than the last one which we are safe to keep.973 for (let ping of pingsToPurge) {974 if (this._shutdown) {975 this._log.trace("_enforcePendingPingsQuota - Terminating the clean up task due to shutdown");976 return;977 }978 // This list is guaranteed to be in order, so remove the pings at its979 // beginning (oldest).980 yield this.removePendingPing(ping.id);981 }982 const endTimeStamp = Policy.now().getTime();983 // We don't know the size of the pending pings directory if we are above the quota,984 // since we stop scanning once we reach the quota. We use a special value to show985 // this condition.986 recordHistograms(PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE, pingsToPurge.length,987 Math.ceil(endTimeStamp - startTimeStamp));988 }),989 /**990 * Reset the storage state in tests.991 */992 reset: function() {993 this._shutdown = false;994 this._scannedArchiveDirectory = false;995 this._archivedPings = new Map();996 this._scannedPendingDirectory = false;997 this._pendingPings = new Map();998 },999 /**1000 * Get a list of info on the archived pings.1001 * This will scan the archive directory and grab basic data about the existing1002 * pings out of their filename.1003 *1004 * @return {promise<sequence<object>>}1005 */1006 loadArchivedPingList: Task.async(function*() {1007 // If there's an archive loading task already running, return it.1008 if (this._scanArchiveTask) {1009 return this._scanArchiveTask;1010 }1011 yield waitForAll(this._activelyArchiving);1012 if (this._scannedArchiveDirectory) {1013 this._log.trace("loadArchivedPingList - Archive already scanned, hitting cache.");1014 return this._archivedPings;1015 }1016 // Since there's no archive loading task running, start it.1017 let result;1018 try {1019 this._scanArchiveTask = this._scanArchive();1020 result = yield this._scanArchiveTask;1021 } finally {1022 this._scanArchiveTask = null;1023 }1024 return result;1025 }),1026 _scanArchive: Task.async(function*() {1027 this._log.trace("_scanArchive");1028 let submitProbes = (pingCount, dirCount) => {1029 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT")1030 .add(pingCount);1031 Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT")1032 .add(dirCount);1033 };1034 if (!(yield OS.File.exists(gPingsArchivePath))) {1035 submitProbes(0, 0);1036 return new Map();1037 }1038 let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath);1039 let subdirs =1040 (yield dirIterator.nextBatch()).filter(e => e.isDir).filter(e => isValidArchiveDir(e.name));1041 dirIterator.close();1042 // Walk through the monthly subdirs of the form <YYYY-MM>/1043 for (let dir of subdirs) {1044 this._log.trace("_scanArchive - checking in subdir: " + dir.path);1045 let pingIterator = new OS.File.DirectoryIterator(dir.path);1046 let pings = (yield pingIterator.nextBatch()).filter(e => !e.isDir);1047 pingIterator.close();1048 // Now process any ping files of the form "<timestamp>.<uuid>.<type>.[json|jsonlz4]".1049 for (let p of pings) {1050 // data may be null if the filename doesn't match the above format.1051 let data = this._getArchivedPingDataFromFileName(p.name);1052 if (!data) {1053 continue;1054 }1055 // In case of conflicts, overwrite only with newer pings.1056 if (this._archivedPings.has(data.id)) {1057 const overwrite = data.timestamp > this._archivedPings.get(data.id).timestampCreated;1058 this._log.warn("_scanArchive - have seen this id before: " + data.id +1059 ", overwrite: " + overwrite);1060 if (!overwrite) {1061 continue;1062 }1063 yield this._removeArchivedPing(data.id, data.timestampCreated, data.type)1064 .catch((e) => this._log.warn("_scanArchive - failed to remove ping", e));1065 }1066 this._archivedPings.set(data.id, {1067 timestampCreated: data.timestamp,1068 type: internString(data.type),1069 });1070 }1071 }1072 // Mark the archive as scanned, so we no longer hit the disk.1073 this._scannedArchiveDirectory = true;1074 // Update the ping and directories count histograms.1075 submitProbes(this._archivedPings.size, subdirs.length);1076 return this._archivedPings;1077 }),1078 /**1079 * Save a single ping to a file.1080 *1081 * @param {object} ping The content of the ping to save.1082 * @param {string} file The destination file.1083 * @param {bool} overwrite If |true|, the file will be overwritten if it exists,1084 * if |false| the file will not be overwritten and no error will be reported if1085 * the file exists.1086 * @param {bool} [compress=false] If |true|, the file will use lz4 compression. Otherwise no1087 * compression will be used.1088 * @returns {promise}1089 */1090 savePingToFile: Task.async(function*(ping, filePath, overwrite, compress = false) {1091 try {1092 this._log.trace("savePingToFile - path: " + filePath);1093 let pingString = JSON.stringify(ping);1094 let options = { tmpPath: filePath + ".tmp", noOverwrite: !overwrite };1095 if (compress) {1096 options.compression = "lz4";1097 }1098 yield OS.File.writeAtomic(filePath, pingString, options);1099 } catch (e) {1100 if (!e.becauseExists) {1101 throw e;1102 }1103 }1104 }),1105 /**1106 * Save a ping to its file.1107 *1108 * @param {object} ping The content of the ping to save.1109 * @param {bool} overwrite If |true|, the file will be overwritten1110 * if it exists.1111 * @returns {promise}1112 */1113 savePing: Task.async(function*(ping, overwrite) {1114 yield getPingDirectory();1115 let file = pingFilePath(ping);1116 yield this.savePingToFile(ping, file, overwrite);1117 return file;1118 }),1119 /**1120 * Add a ping to the saved pings directory so that it gets saved1121 * and sent along with other pings.1122 * Note: that the original ping file will not be modified.1123 *1124 * @param {Object} ping The ping object.1125 * @return {Promise} A promise resolved when the ping is saved to the pings directory.1126 */1127 addPendingPing: function(ping) {1128 return this.savePendingPing(ping);1129 },1130 /**1131 * Remove the file for a ping1132 *1133 * @param {object} ping The ping.1134 * @returns {promise}1135 */1136 cleanupPingFile: function(ping) {1137 return OS.File.remove(pingFilePath(ping));1138 },1139 savePendingPing: function(ping) {1140 let p = this.savePing(ping, true).then((path) => {1141 this._pendingPings.set(ping.id, {1142 path: path,1143 lastModificationDate: Policy.now().getTime(),1144 });1145 this._log.trace("savePendingPing - saved ping with id " + ping.id);1146 });1147 this._trackPendingPingSaveTask(p);1148 return p;1149 },1150 loadPendingPing: Task.async(function*(id) {1151 this._log.trace("loadPendingPing - id: " + id);1152 let info = this._pendingPings.get(id);1153 if (!info) {1154 this._log.trace("loadPendingPing - unknown id " + id);1155 throw new Error("TelemetryStorage.loadPendingPing - no ping with id " + id);1156 }1157 // Try to get the dimension of the ping. If that fails, update the histograms.1158 let fileSize = 0;1159 try {1160 fileSize = (yield OS.File.stat(info.path)).size;1161 } catch (e) {1162 if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile) {1163 throw e;1164 }1165 // Fall through and let |loadPingFile| report the error.1166 }1167 // Purge pings which are too big.1168 if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {1169 yield this.removePendingPing(id);1170 Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB")1171 .add(Math.floor(fileSize / 1024 / 1024));1172 Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add();1173 throw new Error("loadPendingPing - exceeded the maximum ping size: " + fileSize);1174 }1175 // Try to load the ping file. Update the related histograms on failure.1176 let ping;1177 try {1178 ping = yield this.loadPingFile(info.path, false);1179 } catch (e) {1180 // If we failed to load the ping, check what happened and update the histogram.1181 if (e instanceof PingReadError) {1182 Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").add();1183 } else if (e instanceof PingParseError) {1184 Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").add();1185 }1186 // Remove the ping from the cache, so we don't try to load it again.1187 this._pendingPings.delete(id);1188 // Then propagate the rejection.1189 throw e;1190 }1191 return ping;1192 }),1193 removePendingPing: function(id) {1194 let info = this._pendingPings.get(id);1195 if (!info) {1196 this._log.trace("removePendingPing - unknown id " + id);1197 return Promise.resolve();1198 }1199 this._log.trace("removePendingPing - deleting ping with id: " + id +1200 ", path: " + info.path);1201 this._pendingPings.delete(id);1202 return OS.File.remove(info.path).catch((ex) =>1203 this._log.error("removePendingPing - failed to remove ping", ex));1204 },1205 /**1206 * Track any pending ping save tasks through the promise passed here.1207 * This is needed to block on any outstanding ping save activity.1208 *1209 * @param {Object<Promise>} The save promise to track.1210 */1211 _trackPendingPingSaveTask: function (promise) {1212 let clear = () => this._activePendingPingSaves.delete(promise);1213 promise.then(clear, clear);1214 this._activePendingPingSaves.add(promise);1215 },1216 /**1217 * Return a promise that allows to wait on pending pings being saved.1218 * @return {Object<Promise>} A promise resolved when all the pending pings save promises1219 * are resolved.1220 */1221 promisePendingPingSaves: function () {1222 // Make sure to wait for all the promises, even if they reject. We don't need to log1223 // the failures here, as they are already logged elsewhere.1224 return waitForAll(this._activePendingPingSaves);1225 },1226 /**1227 * Run the task to remove all the pending pings (except the deletion ping).1228 *1229 * @return {Promise} Resolved when the pings are removed.1230 */1231 runRemovePendingPingsTask: Task.async(function*() {1232 // If we already have a pending pings removal task active, return that.1233 if (this._removePendingPingsTask) {1234 return this._removePendingPingsTask;1235 }1236 // Start the task to remove all pending pings. Also make sure to clear the task once done.1237 try {1238 this._removePendingPingsTask = this.removePendingPings();1239 yield this._removePendingPingsTask;1240 } finally {1241 this._removePendingPingsTask = null;1242 }1243 return undefined;1244 }),1245 removePendingPings: Task.async(function*() {1246 this._log.trace("removePendingPings - removing all pending pings");1247 // Wait on pending pings still being saved, so so we don't miss removing them.1248 yield this.promisePendingPingSaves();1249 // Individually remove existing pings, so we don't interfere with operations expecting1250 // the pending pings directory to exist.1251 const directory = TelemetryStorage.pingDirectoryPath;1252 let iter = new OS.File.DirectoryIterator(directory);1253 try {1254 if (!(yield iter.exists())) {1255 this._log.trace("removePendingPings - the pending pings directory doesn't exist");1256 return;1257 }1258 let files = (yield iter.nextBatch()).filter(e => !e.isDir);1259 for (let file of files) {1260 try {1261 yield OS.File.remove(file.path);1262 } catch (ex) {1263 this._log.error("removePendingPings - failed to remove file " + file.path, ex);1264 continue;1265 }1266 }1267 } finally {1268 yield iter.close();1269 }1270 }),1271 loadPendingPingList: function() {1272 // If we already have a pending scanning task active, return that.1273 if (this._scanPendingPingsTask) {1274 return this._scanPendingPingsTask;1275 }1276 if (this._scannedPendingDirectory) {1277 this._log.trace("loadPendingPingList - Pending already scanned, hitting cache.");1278 return Promise.resolve(this._buildPingList());1279 }1280 // Since there's no pending pings scan task running, start it.1281 // Also make sure to clear the task once done.1282 this._scanPendingPingsTask = this._scanPendingPings().then(pings => {1283 this._scanPendingPingsTask = null;1284 return pings;1285 }, ex => {1286 this._scanPendingPingsTask = null;1287 throw ex;1288 });1289 return this._scanPendingPingsTask;1290 },1291 getPendingPingList: function() {1292 return this._buildPingList();1293 },1294 _scanPendingPings: Task.async(function*() {1295 this._log.trace("_scanPendingPings");1296 let directory = TelemetryStorage.pingDirectoryPath;1297 let iter = new OS.File.DirectoryIterator(directory);1298 let exists = yield iter.exists();1299 try {1300 if (!exists) {1301 return [];1302 }1303 let files = (yield iter.nextBatch()).filter(e => !e.isDir);1304 for (let file of files) {1305 if (this._shutdown) {1306 return [];1307 }1308 let info;1309 try {1310 info = yield OS.File.stat(file.path);1311 } catch (ex) {1312 this._log.error("_scanPendingPings - failed to stat file " + file.path, ex);1313 continue;1314 }1315 // Enforce a maximum file size limit on pending pings.1316 if (info.size > PING_FILE_MAXIMUM_SIZE_BYTES) {1317 this._log.error("_scanPendingPings - removing file exceeding size limit " + file.path);1318 try {1319 yield OS.File.remove(file.path);1320 } catch (ex) {1321 this._log.error("_scanPendingPings - failed to remove file " + file.path, ex);1322 } finally {1323 Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB")1324 .add(Math.floor(info.size / 1024 / 1024));1325 Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add();1326 continue;1327 }1328 }1329 let id = OS.Path.basename(file.path);1330 if (!UUID_REGEX.test(id)) {1331 this._log.trace("_scanPendingPings - filename is not a UUID: " + id);1332 id = Utils.generateUUID();1333 }1334 this._pendingPings.set(id, {1335 path: file.path,1336 lastModificationDate: info.lastModificationDate.getTime(),1337 });1338 }1339 } finally {1340 yield iter.close();1341 }1342 // Explicitly load the deletion ping from its known path, if it's there.1343 if (yield OS.File.exists(gDeletionPingFilePath)) {1344 this._log.trace("_scanPendingPings - Adding pending deletion ping.");1345 // We can't get the ping id or the last modification date without hitting the disk.1346 // Since deletion has a special handling, we don't really need those.1347 this._pendingPings.set(Utils.generateUUID(), {1348 path: gDeletionPingFilePath,1349 lastModificationDate: Date.now(),1350 });1351 }1352 this._scannedPendingDirectory = true;1353 return this._buildPingList();1354 }),1355 _buildPingList: function() {1356 const list = Array.from(this._pendingPings, p => ({1357 id: p[0],1358 lastModificationDate: p[1].lastModificationDate,1359 }));1360 list.sort((a, b) => b.lastModificationDate - a.lastModificationDate);1361 return list;1362 },1363 get pendingPingCount() {1364 return this._pendingPings.size;1365 },1366 /**1367 * Loads a ping file.1368 * @param {String} aFilePath The path of the ping file.1369 * @param {Boolean} [aCompressed=false] If |true|, expects the file to be compressed using lz4.1370 * @return {Promise<Object>} A promise resolved with the ping content or rejected if the1371 * ping contains invalid data.1372 * @throws {PingReadError} There was an error while reading the ping file from the disk.1373 * @throws {PingParseError} There was an error while parsing the JSON content of the ping file.1374 */1375 loadPingFile: Task.async(function* (aFilePath, aCompressed = false) {1376 let options = {};1377 if (aCompressed) {1378 options.compression = "lz4";1379 }1380 let array;1381 try {1382 array = yield OS.File.read(aFilePath, options);1383 } catch (e) {1384 this._log.trace("loadPingfile - unreadable ping " + aFilePath, e);1385 throw new PingReadError(e.message, e.becauseNoSuchFile);1386 }1387 let decoder = new TextDecoder();1388 let string = decoder.decode(array);1389 let ping;1390 try {1391 ping = JSON.parse(string);1392 } catch (e) {1393 this._log.trace("loadPingfile - unparseable ping " + aFilePath, e);1394 yield OS.File.remove(aFilePath).catch((ex) => {1395 this._log.error("loadPingFile - failed removing unparseable ping file", ex);1396 });1397 throw new PingParseError(e.message);1398 }1399 return ping;1400 }),1401 /**1402 * Archived pings are saved with file names of the form:1403 * "<timestamp>.<uuid>.<type>.[json|jsonlz4]"1404 * This helper extracts that data from a given filename.1405 *1406 * @param fileName {String} The filename.1407 * @return {Object} Null if the filename didn't match the expected form.1408 * Otherwise an object with the extracted data in the form:1409 * { timestamp: <number>,1410 * id: <string>,1411 * type: <string> }1412 */1413 _getArchivedPingDataFromFileName: function(fileName) {1414 // Extract the parts.1415 let parts = fileName.split(".");1416 if (parts.length != 4) {1417 this._log.trace("_getArchivedPingDataFromFileName - should have 4 parts");1418 return null;1419 }1420 let [timestamp, uuid, type, extension] = parts;1421 if (extension != "json" && extension != "jsonlz4") {1422 this._log.trace("_getArchivedPingDataFromFileName - should have 'json' or 'jsonlz4' extension");1423 return null;1424 }1425 // Check for a valid timestamp.1426 timestamp = parseInt(timestamp);1427 if (Number.isNaN(timestamp)) {1428 this._log.trace("_getArchivedPingDataFromFileName - should have a valid timestamp");1429 return null;1430 }1431 // Check for a valid UUID.1432 if (!UUID_REGEX.test(uuid)) {1433 this._log.trace("_getArchivedPingDataFromFileName - should have a valid id");1434 return null;1435 }1436 // Check for a valid type string.1437 const typeRegex = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;1438 if (!typeRegex.test(type)) {1439 this._log.trace("_getArchivedPingDataFromFileName - should have a valid type");1440 return null;1441 }1442 return {1443 timestamp: timestamp,1444 id: uuid,1445 type: type,1446 };1447 },1448 saveAbortedSessionPing: Task.async(function*(ping) {1449 this._log.trace("saveAbortedSessionPing - ping path: " + gAbortedSessionFilePath);1450 yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });1451 return this._abortedSessionSerializer.enqueueTask(() =>1452 this.savePingToFile(ping, gAbortedSessionFilePath, true));1453 }),1454 loadAbortedSessionPing: Task.async(function*() {1455 let ping = null;1456 try {1457 ping = yield this.loadPingFile(gAbortedSessionFilePath);1458 } catch (ex) {1459 if (ex.becauseNoSuchFile) {1460 this._log.trace("loadAbortedSessionPing - no such file");1461 } else {1462 this._log.error("loadAbortedSessionPing - error loading ping", ex)1463 }1464 }1465 return ping;1466 }),1467 removeAbortedSessionPing: function() {1468 return this._abortedSessionSerializer.enqueueTask(Task.async(function*() {1469 try {1470 yield OS.File.remove(gAbortedSessionFilePath, { ignoreAbsent: false });1471 this._log.trace("removeAbortedSessionPing - success");1472 } catch (ex) {1473 if (ex.becauseNoSuchFile) {1474 this._log.trace("removeAbortedSessionPing - no such file");1475 } else {1476 this._log.error("removeAbortedSessionPing - error removing ping", ex)1477 }1478 }1479 }.bind(this)));1480 },1481 /**1482 * Save the deletion ping.1483 * @param ping The deletion ping.1484 * @return {Promise} Resolved when the ping is saved.1485 */1486 saveDeletionPing: Task.async(function*(ping) {1487 this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath);1488 yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });1489 let p = this._deletionPingSerializer.enqueueTask(() =>1490 this.savePingToFile(ping, gDeletionPingFilePath, true));1491 this._trackPendingPingSaveTask(p);1492 return p;1493 }),1494 /**1495 * Remove the deletion ping.1496 * @return {Promise} Resolved when the ping is deleted from the disk.1497 */1498 removeDeletionPing: Task.async(function*() {1499 return this._deletionPingSerializer.enqueueTask(Task.async(function*() {1500 try {1501 yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false });1502 this._log.trace("removeDeletionPing - success");1503 } catch (ex) {1504 if (ex.becauseNoSuchFile) {1505 this._log.trace("removeDeletionPing - no such file");1506 } else {1507 this._log.error("removeDeletionPing - error removing ping", ex)1508 }1509 }1510 }.bind(this)));1511 }),1512 isDeletionPing: function(aPingId) {1513 this._log.trace("isDeletionPing - id: " + aPingId);1514 let pingInfo = this._pendingPings.get(aPingId);1515 if (!pingInfo) {1516 return false;1517 }1518 if (pingInfo.path != gDeletionPingFilePath) {1519 return false;1520 }1521 return true;1522 },1523 /**1524 * Remove FHR database files. This is temporary and will be dropped in1525 * the future.1526 * @return {Promise} Resolved when the database files are deleted.1527 */1528 removeFHRDatabase: Task.async(function*() {1529 this._log.trace("removeFHRDatabase");1530 // Let's try to remove the FHR DB with the default filename first.1531 const FHR_DB_DEFAULT_FILENAME = "healthreport.sqlite";1532 // Even if it's uncommon, there may be 2 additional files: - a "write ahead log"1533 // (-wal) file and a "shared memory file" (-shm). We need to remove them as well.1534 let FILES_TO_REMOVE = [1535 OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME),1536 OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME + "-wal"),1537 OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_DEFAULT_FILENAME + "-shm"),1538 ];1539 // FHR could have used either the default DB file name or a custom one1540 // through this preference.1541 const FHR_DB_CUSTOM_FILENAME =1542 Preferences.get("datareporting.healthreport.dbName", undefined);1543 if (FHR_DB_CUSTOM_FILENAME) {1544 FILES_TO_REMOVE.push(1545 OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME),1546 OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME + "-wal"),1547 OS.Path.join(OS.Constants.Path.profileDir, FHR_DB_CUSTOM_FILENAME + "-shm"));1548 }1549 for (let f of FILES_TO_REMOVE) {1550 yield OS.File.remove(f, {ignoreAbsent: true})1551 .catch(e => this._log.error("removeFHRDatabase - failed to remove " + f, e));1552 }1553 }),1554};1555// Utility functions1556function pingFilePath(ping) {1557 // Support legacy ping formats, who don't have an "id" field, but a "slug" field.1558 let pingIdentifier = (ping.slug) ? ping.slug : ping.id;1559 return OS.Path.join(TelemetryStorage.pingDirectoryPath, pingIdentifier);1560}1561function getPingDirectory() {1562 return Task.spawn(function*() {1563 let directory = TelemetryStorage.pingDirectoryPath;1564 if (!(yield OS.File.exists(directory))) {1565 yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU });1566 }1567 return directory;1568 });1569}1570/**1571 * Build the path to the archived ping.1572 * @param {String} aPingId The ping id.1573 * @param {Object} aDate The ping creation date.1574 * @param {String} aType The ping type.1575 * @return {String} The full path to the archived ping.1576 */1577function getArchivedPingPath(aPingId, aDate, aType) {1578 // Helper to pad the month to 2 digits, if needed (e.g. "1" -> "01").1579 let addLeftPadding = value => (value < 10) ? ("0" + value) : value;1580 // Get the ping creation date and generate the archive directory to hold it. Note1581 // that getMonth returns a 0-based month, so we need to add an offset.1582 let archivedPingDir = OS.Path.join(gPingsArchivePath,1583 aDate.getFullYear() + '-' + addLeftPadding(aDate.getMonth() + 1));1584 // Generate the archived ping file path as YYYY-MM/<TIMESTAMP>.UUID.type.json1585 let fileName = [aDate.getTime(), aPingId, aType, "json"].join(".");1586 return OS.Path.join(archivedPingDir, fileName);1587}1588/**1589 * Get the size of the ping file on the disk.1590 * @return {Integer} The file size, in bytes, of the ping file or 0 on errors.1591 */1592var getArchivedPingSize = Task.async(function*(aPingId, aDate, aType) {1593 const path = getArchivedPingPath(aPingId, aDate, aType);1594 let filePaths = [ path + "lz4", path ];1595 for (let path of filePaths) {1596 try {1597 return (yield OS.File.stat(path)).size;1598 } catch (e) {}1599 }1600 // That's odd, this ping doesn't seem to exist.1601 return 0;1602});1603/**1604 * Get the size of the pending ping file on the disk.1605 * @return {Integer} The file size, in bytes, of the ping file or 0 on errors.1606 */1607var getPendingPingSize = Task.async(function*(aPingId) {1608 const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, aPingId)1609 try {1610 return (yield OS.File.stat(path)).size;1611 } catch (e) {}1612 // That's odd, this ping doesn't seem to exist.1613 return 0;1614});1615/**1616 * Check if a directory name is in the "YYYY-MM" format.1617 * @param {String} aDirName The name of the pings archive directory.1618 * @return {Boolean} True if the directory name is in the right format, false otherwise.1619 */1620function isValidArchiveDir(aDirName) {1621 const dirRegEx = /^[0-9]{4}-[0-9]{2}$/;1622 return dirRegEx.test(aDirName);1623}1624/**1625 * Gets a date object from an archive directory name.1626 * @param {String} aDirName The name of the pings archive directory. Must be in the YYYY-MM1627 * format.1628 * @return {Object} A Date object or null if the dir name is not valid.1629 */1630function getDateFromArchiveDir(aDirName) {1631 let [year, month] = aDirName.split("-");1632 year = parseInt(year);1633 month = parseInt(month);1634 // Make sure to have sane numbers.1635 if (!Number.isFinite(month) || !Number.isFinite(year) || month < 1 || month > 12) {1636 return null;1637 }1638 return new Date(year, month - 1, 1, 0, 0, 0);...
test_TelemetrySendOldPings.js
Source:test_TelemetrySendOldPings.js
1/* Any copyright is dedicated to the Public Domain.2 http://creativecommons.org/publicdomain/zero/1.0/3/**4 * This test case populates the profile with some fake stored5 * pings, and checks that pending pings are immediatlely sent6 * after delayed init.7 */8"use strict"9Cu.import("resource://gre/modules/osfile.jsm", this);10Cu.import("resource://gre/modules/Services.jsm", this);11Cu.import("resource://gre/modules/Promise.jsm", this);12Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);13Cu.import("resource://gre/modules/TelemetryController.jsm", this);14Cu.import("resource://gre/modules/TelemetrySend.jsm", this);15Cu.import("resource://gre/modules/Task.jsm", this);16Cu.import("resource://gre/modules/XPCOMUtils.jsm");17var {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {});18// We increment TelemetryStorage's MAX_PING_FILE_AGE and19// OVERDUE_PING_FILE_AGE by 1 minute so that our test pings exceed20// those points in time, even taking into account file system imprecision.21const ONE_MINUTE_MS = 60 * 1000;22const OVERDUE_PING_FILE_AGE = TelemetrySend.OVERDUE_PING_FILE_AGE + ONE_MINUTE_MS;23const PING_SAVE_FOLDER = "saved-telemetry-pings";24const PING_TIMEOUT_LENGTH = 5000;25const OVERDUE_PINGS = 6;26const OLD_FORMAT_PINGS = 4;27const RECENT_PINGS = 4;28const TOTAL_EXPECTED_PINGS = OVERDUE_PINGS + RECENT_PINGS + OLD_FORMAT_PINGS;29const PREF_FHR_UPLOAD = "datareporting.healthreport.uploadEnabled";30var gCreatedPings = 0;31var gSeenPings = 0;32/**33 * Creates some Telemetry pings for the and saves them to disk. Each ping gets a34 * unique ID based on an incrementor.35 *36 * @param {Array} aPingInfos An array of ping type objects. Each entry must be an37 * object containing a "num" field for the number of pings to create and38 * an "age" field. The latter representing the age in milliseconds to offset39 * from now. A value of 10 would make the ping 10ms older than now, for40 * example.41 * @returns Promise42 * @resolve an Array with the created pings ids.43 */44var createSavedPings = Task.async(function* (aPingInfos) {45 let pingIds = [];46 let now = Date.now();47 for (let type in aPingInfos) {48 let num = aPingInfos[type].num;49 let age = now - (aPingInfos[type].age || 0);50 for (let i = 0; i < num; ++i) {51 let pingId = yield TelemetryController.addPendingPing("test-ping", {}, { overwrite: true });52 if (aPingInfos[type].age) {53 // savePing writes to the file synchronously, so we're good to54 // modify the lastModifedTime now.55 let filePath = getSavePathForPingId(pingId);56 yield File.setDates(filePath, null, age);57 }58 gCreatedPings++;59 pingIds.push(pingId);60 }61 }62 return pingIds;63});64/**65 * Deletes locally saved pings if they exist.66 *67 * @param aPingIds an Array of ping ids to delete.68 * @returns Promise69 */70var clearPings = Task.async(function* (aPingIds) {71 for (let pingId of aPingIds) {72 yield TelemetryStorage.removePendingPing(pingId);73 }74});75/**76 * Fakes the pending pings storage quota.77 * @param {Integer} aPendingQuota The new quota, in bytes.78 */79function fakePendingPingsQuota(aPendingQuota) {80 let storage = Cu.import("resource://gre/modules/TelemetryStorage.jsm");81 storage.Policy.getPendingPingsQuota = () => aPendingQuota;82}83/**84 * Returns a handle for the file that a ping should be85 * stored in locally.86 *87 * @returns path88 */89function getSavePathForPingId(aPingId) {90 return Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, aPingId);91}92/**93 * Check if the number of Telemetry pings received by the HttpServer is not equal94 * to aExpectedNum.95 *96 * @param aExpectedNum the number of pings we expect to receive.97 */98function assertReceivedPings(aExpectedNum) {99 do_check_eq(gSeenPings, aExpectedNum);100}101/**102 * Throws if any pings with the id in aPingIds is saved locally.103 *104 * @param aPingIds an Array of pings ids to check.105 * @returns Promise106 */107var assertNotSaved = Task.async(function* (aPingIds) {108 let saved = 0;109 for (let id of aPingIds) {110 let filePath = getSavePathForPingId(id);111 if (yield File.exists(filePath)) {112 saved++;113 }114 }115 if (saved > 0) {116 do_throw("Found " + saved + " unexpected saved pings.");117 }118});119/**120 * Our handler function for the HttpServer that simply121 * increments the gSeenPings global when it successfully122 * receives and decodes a Telemetry payload.123 *124 * @param aRequest the HTTP request sent from HttpServer.125 */126function pingHandler(aRequest) {127 gSeenPings++;128}129add_task(function* test_setup() {130 PingServer.start();131 PingServer.registerPingHandler(pingHandler);132 do_get_profile();133 loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");134 // Make sure we don't generate unexpected pings due to pref changes.135 yield setEmptyPrefWatchlist();136 Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);137 Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER,138 "http://localhost:" + PingServer.port);139});140/**141 * Setup the tests by making sure the ping storage directory is available, otherwise142 * |TelemetryController.testSaveDirectoryToFile| could fail.143 */144add_task(function* setupEnvironment() {145 // The following tests assume this pref to be true by default.146 Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true);147 yield TelemetryController.testSetup();148 let directory = TelemetryStorage.pingDirectoryPath;149 yield File.makeDir(directory, { ignoreExisting: true, unixMode: OS.Constants.S_IRWXU });150 yield TelemetryStorage.testClearPendingPings();151});152/**153 * Test that really recent pings are sent on Telemetry initialization.154 */155add_task(function* test_recent_pings_sent() {156 let pingTypes = [{ num: RECENT_PINGS }];157 yield createSavedPings(pingTypes);158 yield TelemetryController.testReset();159 yield TelemetrySend.testWaitOnOutgoingPings();160 assertReceivedPings(RECENT_PINGS);161 yield TelemetryStorage.testClearPendingPings();162});163/**164 * Create an overdue ping in the old format and try to send it.165 */166add_task(function* test_overdue_old_format() {167 // A test ping in the old, standard format.168 const PING_OLD_FORMAT = {169 slug: "1234567abcd",170 reason: "test-ping",171 payload: {172 info: {173 reason: "test-ping",174 OS: "XPCShell",175 appID: "SomeId",176 appVersion: "1.0",177 appName: "XPCShell",178 appBuildID: "123456789",179 appUpdateChannel: "Test",180 platformBuildID: "987654321",181 },182 },183 };184 // A ping with no info section, but with a slug.185 const PING_NO_INFO = {186 slug: "1234-no-info-ping",187 reason: "test-ping",188 payload: {}189 };190 // A ping with no payload.191 const PING_NO_PAYLOAD = {192 slug: "5678-no-payload",193 reason: "test-ping",194 };195 // A ping with no info and no slug.196 const PING_NO_SLUG = {197 reason: "test-ping",198 payload: {}199 };200 const PING_FILES_PATHS = [201 getSavePathForPingId(PING_OLD_FORMAT.slug),202 getSavePathForPingId(PING_NO_INFO.slug),203 getSavePathForPingId(PING_NO_PAYLOAD.slug),204 getSavePathForPingId("no-slug-file"),205 ];206 // Write the ping to file and make it overdue.207 yield TelemetryStorage.savePing(PING_OLD_FORMAT, true);208 yield TelemetryStorage.savePing(PING_NO_INFO, true);209 yield TelemetryStorage.savePing(PING_NO_PAYLOAD, true);210 yield TelemetryStorage.savePingToFile(PING_NO_SLUG, PING_FILES_PATHS[3], true);211 for (let f in PING_FILES_PATHS) {212 yield File.setDates(PING_FILES_PATHS[f], null, Date.now() - OVERDUE_PING_FILE_AGE);213 }214 gSeenPings = 0;215 yield TelemetryController.testReset();216 yield TelemetrySend.testWaitOnOutgoingPings();217 assertReceivedPings(OLD_FORMAT_PINGS);218 // |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id,219 // so remove it manually so that the next test doesn't fail.220 yield OS.File.remove(PING_FILES_PATHS[3]);221 yield TelemetryStorage.testClearPendingPings();222});223add_task(function* test_corrupted_pending_pings() {224 const TEST_TYPE = "test_corrupted";225 Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").clear();226 Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").clear();227 // Save a pending ping and get its id.228 let pendingPingId = yield TelemetryController.addPendingPing(TEST_TYPE, {}, {});229 // Try to load it: there should be no error.230 yield TelemetryStorage.loadPendingPing(pendingPingId);231 let h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();232 Assert.equal(h.sum, 0, "Telemetry must not report a pending ping load failure");233 h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();234 Assert.equal(h.sum, 0, "Telemetry must not report a pending ping parse failure");235 // Delete it from the disk, so that its id will be kept in the cache but it will236 // fail loading the file.237 yield OS.File.remove(getSavePathForPingId(pendingPingId));238 // Try to load a pending ping which isn't there anymore.239 yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId),240 "Telemetry must fail loading a ping which isn't there");241 h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();242 Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");243 h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();244 Assert.equal(h.sum, 0, "Telemetry must not report a pending ping parse failure");245 // Save a new ping, so that it gets in the pending pings cache.246 pendingPingId = yield TelemetryController.addPendingPing(TEST_TYPE, {}, {});247 // Overwrite it with a corrupted JSON file and then try to load it.248 const INVALID_JSON = "{ invalid,JSON { {1}";249 yield OS.File.writeAtomic(getSavePathForPingId(pendingPingId), INVALID_JSON, { encoding: "utf-8" });250 // Try to load the ping with the corrupted JSON content.251 yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId),252 "Telemetry must fail loading a corrupted ping");253 h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();254 Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");255 h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();256 Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure");257 let exists = yield OS.File.exists(getSavePathForPingId(pendingPingId));258 Assert.ok(!exists, "The unparseable ping should have been removed");259 yield TelemetryStorage.testClearPendingPings();260});261/**262 * Create some recent and overdue pings and verify that they get sent.263 */264add_task(function* test_overdue_pings_trigger_send() {265 let pingTypes = [266 { num: RECENT_PINGS },267 { num: OVERDUE_PINGS, age: OVERDUE_PING_FILE_AGE },268 ];269 let pings = yield createSavedPings(pingTypes);270 let recentPings = pings.slice(0, RECENT_PINGS);271 let overduePings = pings.slice(-OVERDUE_PINGS);272 yield TelemetryController.testReset();273 yield TelemetrySend.testWaitOnOutgoingPings();274 assertReceivedPings(TOTAL_EXPECTED_PINGS);275 yield assertNotSaved(recentPings);276 yield assertNotSaved(overduePings);277 Assert.equal(TelemetrySend.overduePingsCount, overduePings.length,278 "Should have tracked the correct amount of overdue pings");279 yield TelemetryStorage.testClearPendingPings();280});281/**282 * Create a ping in the old format, send it, and make sure the request URL contains283 * the correct version query parameter.284 */285add_task(function* test_overdue_old_format() {286 // A test ping in the old, standard format.287 const PING_OLD_FORMAT = {288 slug: "1234567abcd",289 reason: "test-ping",290 payload: {291 info: {292 reason: "test-ping",293 OS: "XPCShell",294 appID: "SomeId",295 appVersion: "1.0",296 appName: "XPCShell",297 appBuildID: "123456789",298 appUpdateChannel: "Test",299 platformBuildID: "987654321",300 },301 },302 };303 const filePath =304 Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, PING_OLD_FORMAT.slug);305 // Write the ping to file and make it overdue.306 yield TelemetryStorage.savePing(PING_OLD_FORMAT, true);307 yield File.setDates(filePath, null, Date.now() - OVERDUE_PING_FILE_AGE);308 let receivedPings = 0;309 // Register a new prefix handler to validate the URL.310 PingServer.registerPingHandler(request => {311 // Check that we have a version query parameter in the URL.312 Assert.notEqual(request.queryString, "");313 // Make sure the version in the query string matches the old ping format version.314 let params = request.queryString.split("&");315 Assert.ok(params.find(p => p == "v=1"));316 receivedPings++;317 });318 yield TelemetryController.testReset();319 yield TelemetrySend.testWaitOnOutgoingPings();320 Assert.equal(receivedPings, 1, "We must receive a ping in the old format.");321 yield TelemetryStorage.testClearPendingPings();322 PingServer.resetPingHandler();323});324add_task(function* test_pendingPingsQuota() {325 const PING_TYPE = "foo";326 // Disable upload so pings don't get sent and removed from the pending pings directory.327 Services.prefs.setBoolPref(PREF_FHR_UPLOAD, false);328 // Remove all the pending pings then startup and wait for the cleanup task to complete.329 // There should be nothing to remove.330 yield TelemetryStorage.testClearPendingPings();331 yield TelemetryController.testReset();332 yield TelemetrySend.testWaitOnOutgoingPings();333 yield TelemetryStorage.testPendingQuotaTaskPromise();334 // Remove the pending deletion ping generated when flipping FHR upload off.335 yield TelemetryStorage.testClearPendingPings();336 let expectedPrunedPings = [];337 let expectedNotPrunedPings = [];338 let checkPendingPings = Task.async(function*() {339 // Check that the pruned pings are not on disk anymore.340 for (let prunedPingId of expectedPrunedPings) {341 yield Assert.rejects(TelemetryStorage.loadPendingPing(prunedPingId),342 "Ping " + prunedPingId + " should have been pruned.");343 const pingPath = getSavePathForPingId(prunedPingId);344 Assert.ok(!(yield OS.File.exists(pingPath)), "The ping should not be on the disk anymore.");345 }346 // Check that the expected pings are there.347 for (let expectedPingId of expectedNotPrunedPings) {348 Assert.ok((yield TelemetryStorage.loadPendingPing(expectedPingId)),349 "Ping" + expectedPingId + " should be among the pending pings.");350 }351 });352 let pendingPingsInfo = [];353 let pingsSizeInBytes = 0;354 // Create 10 pings to test the pending pings quota.355 for (let days = 1; days < 11; days++) {356 const date = fakeNow(2010, 1, days, 1, 1, 0);357 const pingId = yield TelemetryController.addPendingPing(PING_TYPE, {}, {});358 // Find the size of the ping.359 const pingFilePath = getSavePathForPingId(pingId);360 const pingSize = (yield OS.File.stat(pingFilePath)).size;361 // Add the info at the beginning of the array, so that most recent pings come first.362 pendingPingsInfo.unshift({id: pingId, size: pingSize, timestamp: date.getTime() });363 // Set the last modification date.364 yield OS.File.setDates(pingFilePath, null, date.getTime());365 // Add it to the pending ping directory size.366 pingsSizeInBytes += pingSize;367 }368 // We need to test the pending pings size before we hit the quota, otherwise a special369 // value is recorded.370 Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").clear();371 Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").clear();372 Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").clear();373 yield TelemetryController.testReset();374 yield TelemetryStorage.testPendingQuotaTaskPromise();375 // Check that the correct values for quota probes are reported when no quota is hit.376 let h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();377 Assert.equal(h.sum, Math.round(pingsSizeInBytes / 1024 / 1024),378 "Telemetry must report the correct pending pings directory size.");379 h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot();380 Assert.equal(h.sum, 0, "Telemetry must report 0 evictions if quota is not hit.");381 h = Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").snapshot();382 Assert.equal(h.sum, 0, "Telemetry must report a null elapsed time if quota is not hit.");383 // Set the quota to 80% of the space.384 const testQuotaInBytes = pingsSizeInBytes * 0.8;385 fakePendingPingsQuota(testQuotaInBytes);386 // The storage prunes pending pings until we reach 90% of the requested storage quota.387 // Based on that, find how many pings should be kept.388 const safeQuotaSize = Math.round(testQuotaInBytes * 0.9);389 let sizeInBytes = 0;390 let pingsWithinQuota = [];391 let pingsOutsideQuota = [];392 for (let pingInfo of pendingPingsInfo) {393 sizeInBytes += pingInfo.size;394 if (sizeInBytes >= safeQuotaSize) {395 pingsOutsideQuota.push(pingInfo.id);396 continue;397 }398 pingsWithinQuota.push(pingInfo.id);399 }400 expectedNotPrunedPings = pingsWithinQuota;401 expectedPrunedPings = pingsOutsideQuota;402 // Reset TelemetryController to start the pending pings cleanup.403 yield TelemetryController.testReset();404 yield TelemetryStorage.testPendingQuotaTaskPromise();405 yield checkPendingPings();406 h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot();407 Assert.equal(h.sum, pingsOutsideQuota.length,408 "Telemetry must correctly report the over quota pings evicted from the pending pings directory.");409 h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();410 Assert.equal(h.sum, 17, "Pending pings quota was hit, a special size must be reported.");411 // Trigger a cleanup again and make sure we're not removing anything.412 yield TelemetryController.testReset();413 yield TelemetryStorage.testPendingQuotaTaskPromise();414 yield checkPendingPings();415 const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";416 // Create a pending oversized ping.417 const OVERSIZED_PING = {418 id: OVERSIZED_PING_ID,419 type: PING_TYPE,420 creationDate: (new Date()).toISOString(),421 // Generate a 2MB string to use as the ping payload.422 payload: generateRandomString(2 * 1024 * 1024),423 };424 yield TelemetryStorage.savePendingPing(OVERSIZED_PING);425 // Reset the histograms.426 Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").clear();427 Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").clear();428 // Try to manually load the oversized ping.429 yield Assert.rejects(TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID),430 "The oversized ping should have been pruned.");431 Assert.ok(!(yield OS.File.exists(getSavePathForPingId(OVERSIZED_PING_ID))),432 "The ping should not be on the disk anymore.");433 // Make sure we're correctly updating the related histograms.434 h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot();435 Assert.equal(h.sum, 1, "Telemetry must report 1 oversized ping in the pending pings directory.");436 h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot();437 Assert.equal(h.counts[2], 1, "Telemetry must report a 2MB, oversized, ping.");438 // Save the ping again to check if it gets pruned when scanning the pings directory.439 yield TelemetryStorage.savePendingPing(OVERSIZED_PING);440 expectedPrunedPings.push(OVERSIZED_PING_ID);441 // Scan the pending pings directory.442 yield TelemetryController.testReset();443 yield TelemetryStorage.testPendingQuotaTaskPromise();444 yield checkPendingPings();445 // Make sure we're correctly updating the related histograms.446 h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot();447 Assert.equal(h.sum, 2, "Telemetry must report 1 oversized ping in the pending pings directory.");448 h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot();449 Assert.equal(h.counts[2], 2, "Telemetry must report two 2MB, oversized, pings.");450 Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true);451});452add_task(function* teardown() {453 yield PingServer.stop();...
test_TelemetryController.js
Source:test_TelemetryController.js
1/* Any copyright is dedicated to the Public Domain.2 http://creativecommons.org/publicdomain/zero/1.0/3*/4/* This testcase triggers two telemetry pings.5 *6 * Telemetry code keeps histograms of past telemetry pings. The first7 * ping populates these histograms. One of those histograms is then8 * checked in the second request.9 */10Cu.import("resource://gre/modules/ClientID.jsm");11Cu.import("resource://gre/modules/Services.jsm");12Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);13Cu.import("resource://gre/modules/TelemetryController.jsm", this);14Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);15Cu.import("resource://gre/modules/TelemetrySend.jsm", this);16Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);17Cu.import("resource://gre/modules/Task.jsm", this);18Cu.import("resource://gre/modules/Promise.jsm", this);19Cu.import("resource://gre/modules/Preferences.jsm");20const PING_FORMAT_VERSION = 4;21const DELETION_PING_TYPE = "deletion";22const TEST_PING_TYPE = "test-ping-type";23const PLATFORM_VERSION = "1.9.2";24const APP_VERSION = "1";25const APP_NAME = "XPCShell";26const PREF_BRANCH = "toolkit.telemetry.";27const PREF_ENABLED = PREF_BRANCH + "enabled";28const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";29const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";30const PREF_UNIFIED = PREF_BRANCH + "unified";31var gClientID = null;32function sendPing(aSendClientId, aSendEnvironment) {33 if (PingServer.started) {34 TelemetrySend.setServer("http://localhost:" + PingServer.port);35 } else {36 TelemetrySend.setServer("http://doesnotexist");37 }38 let options = {39 addClientId: aSendClientId,40 addEnvironment: aSendEnvironment,41 };42 return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);43}44function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {45 const MANDATORY_PING_FIELDS = [46 "type", "id", "creationDate", "version", "application", "payload"47 ];48 const APPLICATION_TEST_DATA = {49 buildId: gAppInfo.appBuildID,50 name: APP_NAME,51 version: APP_VERSION,52 displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,53 vendor: "Mozilla",54 platformVersion: PLATFORM_VERSION,55 xpcomAbi: "noarch-spidermonkey",56 };57 // Check that the ping contains all the mandatory fields.58 for (let f of MANDATORY_PING_FIELDS) {59 Assert.ok(f in aPing, f + " must be available.");60 }61 Assert.equal(aPing.type, aType, "The ping must have the correct type.");62 Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version.");63 // Test the application section.64 for (let f in APPLICATION_TEST_DATA) {65 Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f],66 f + " must have the correct value.");67 }68 // We can't check the values for channel and architecture. Just make69 // sure they are in.70 Assert.ok("architecture" in aPing.application,71 "The application section must have an architecture field.");72 Assert.ok("channel" in aPing.application,73 "The application section must have a channel field.");74 // Check the clientId and environment fields, as needed.75 Assert.equal("clientId" in aPing, aHasClientId);76 Assert.equal("environment" in aPing, aHasEnvironment);77}78add_task(function* test_setup() {79 // Addon manager needs a profile directory80 do_get_profile();81 loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");82 // Make sure we don't generate unexpected pings due to pref changes.83 yield setEmptyPrefWatchlist();84 Services.prefs.setBoolPref(PREF_ENABLED, true);85 Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);86 yield new Promise(resolve =>87 Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve)));88});89add_task(function* asyncSetup() {90 yield TelemetryController.testSetup();91});92// Ensure that not overwriting an existing file fails silently93add_task(function* test_overwritePing() {94 let ping = {id: "foo"};95 yield TelemetryStorage.savePing(ping, true);96 yield TelemetryStorage.savePing(ping, false);97 yield TelemetryStorage.cleanupPingFile(ping);98});99// Checks that a sent ping is correctly received by a dummy http server.100add_task(function* test_simplePing() {101 PingServer.start();102 // Update the Telemetry Server preference with the address of the local server.103 // Otherwise we might end up sending stuff to a non-existing server after104 // |TelemetryController.testReset| is called.105 Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port);106 yield sendPing(false, false);107 let request = yield PingServer.promiseNextRequest();108 // Check that we have a version query parameter in the URL.109 Assert.notEqual(request.queryString, "");110 // Make sure the version in the query string matches the new ping format version.111 let params = request.queryString.split("&");112 Assert.ok(params.find(p => p == ("v=" + PING_FORMAT_VERSION)));113 let ping = decodeRequestPayload(request);114 checkPingFormat(ping, TEST_PING_TYPE, false, false);115});116add_task(function* test_disableDataUpload() {117 const isUnified = Preferences.get(PREF_UNIFIED, false);118 if (!isUnified) {119 // Skipping the test if unified telemetry is off, as no deletion ping will120 // be generated.121 return;122 }123 // Disable FHR upload: this should trigger a deletion ping.124 Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);125 let ping = yield PingServer.promiseNextPing();126 checkPingFormat(ping, DELETION_PING_TYPE, true, false);127 // Wait on ping activity to settle.128 yield TelemetrySend.testWaitOnOutgoingPings();129 // Restore FHR Upload.130 Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);131 // Simulate a failure in sending the deletion ping by disabling the HTTP server.132 yield PingServer.stop();133 // Try to send a ping. It will be saved as pending and get deleted when disabling upload.134 TelemetryController.submitExternalPing(TEST_PING_TYPE, {});135 // Disable FHR upload to send a deletion ping again.136 Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);137 // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that.138 yield TelemetrySend.testWaitOnOutgoingPings();139 // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't140 // trigger the shutdown, so we need to call it ourselves.141 yield TelemetryStorage.shutdown();142 // Simulate a restart, and spin the send task.143 yield TelemetryController.testReset();144 // Disabling Telemetry upload must clear out all the pending pings.145 let pendingPings = yield TelemetryStorage.loadPendingPingList();146 Assert.equal(pendingPings.length, 1,147 "All the pending pings but the deletion ping should have been deleted");148 // Enable the ping server again.149 PingServer.start();150 // We set the new server using the pref, otherwise it would get reset with151 // |TelemetryController.testReset|.152 Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port);153 // Stop the sending task and then start it again.154 yield TelemetrySend.shutdown();155 // Reset the controller to spin the ping sending task.156 yield TelemetryController.testReset();157 ping = yield PingServer.promiseNextPing();158 checkPingFormat(ping, DELETION_PING_TYPE, true, false);159 // Wait on ping activity to settle before moving on to the next test. If we were160 // to shut down telemetry, even though the PingServer caught the expected pings,161 // TelemetrySend could still be processing them (clearing pings would happen in162 // a couple of ticks). Shutting down would cancel the request and save them as163 // pending pings.164 yield TelemetrySend.testWaitOnOutgoingPings();165 // Restore FHR Upload.166 Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);167});168add_task(function* test_pingHasClientId() {169 const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";170 // Make sure we have no cached client ID for this test: we'll try to send171 // a ping with it while Telemetry is being initialized.172 Preferences.reset(PREF_CACHED_CLIENTID);173 yield TelemetryController.testShutdown();174 yield ClientID._reset();175 yield TelemetryStorage.testClearPendingPings();176 // And also clear the counter histogram since we're here.177 let h = Telemetry.getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID");178 h.clear();179 // Init telemetry and try to send a ping with a client ID.180 let promisePingSetup = TelemetryController.testReset();181 yield sendPing(true, false);182 Assert.equal(h.snapshot().sum, 1,183 "We must have a ping waiting for the clientId early during startup.");184 // Wait until we are fully initialized. Pings will be assembled but won't get185 // sent before then.186 yield promisePingSetup;187 let ping = yield PingServer.promiseNextPing();188 // Fetch the client ID after initializing and fetching the the ping, so we189 // don't unintentionally trigger its loading. We'll still need the client ID190 // to see if the ping looks sane.191 gClientID = yield ClientID.getClientID();192 checkPingFormat(ping, TEST_PING_TYPE, true, false);193 Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");194 // Shutdown Telemetry so we can safely restart it.195 yield TelemetryController.testShutdown();196 yield TelemetryStorage.testClearPendingPings();197 // We should have cached the client ID now. Lets confirm that by checking it before198 // the async ping setup is finished.199 h.clear();200 promisePingSetup = TelemetryController.testReset();201 yield sendPing(true, false);202 yield promisePingSetup;203 // Check that we received the cached client id.204 Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId.");205 ping = yield PingServer.promiseNextPing();206 checkPingFormat(ping, TEST_PING_TYPE, true, false);207 Assert.equal(ping.clientId, gClientID,208 "Telemetry should report the correct cached clientId.");209 // Check that sending a ping without relying on the cache, after the210 // initialization, still works.211 Preferences.reset(PREF_CACHED_CLIENTID);212 yield TelemetryController.testShutdown();213 yield TelemetryStorage.testClearPendingPings();214 yield TelemetryController.testReset();215 yield sendPing(true, false);216 ping = yield PingServer.promiseNextPing();217 checkPingFormat(ping, TEST_PING_TYPE, true, false);218 Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");219 Assert.equal(h.snapshot().sum, 0, "No ping should have been waiting for a clientId.");220});221add_task(function* test_pingHasEnvironment() {222 // Send a ping with the environment data.223 yield sendPing(false, true);224 let ping = yield PingServer.promiseNextPing();225 checkPingFormat(ping, TEST_PING_TYPE, false, true);226 // Test a field in the environment build section.227 Assert.equal(ping.application.buildId, ping.environment.build.buildId);228});229add_task(function* test_pingHasEnvironmentAndClientId() {230 // Send a ping with the environment data and client id.231 yield sendPing(true, true);232 let ping = yield PingServer.promiseNextPing();233 checkPingFormat(ping, TEST_PING_TYPE, true, true);234 // Test a field in the environment build section.235 Assert.equal(ping.application.buildId, ping.environment.build.buildId);236 // Test that we have the correct clientId.237 Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported.");238});239add_task(function* test_archivePings() {240 let now = new Date(2009, 10, 18, 12, 0, 0);241 fakeNow(now);242 // Disable ping upload so that pings don't get sent.243 // With unified telemetry the FHR upload pref controls this,244 // with non-unified telemetry the Telemetry enabled pref.245 const isUnified = Preferences.get(PREF_UNIFIED, false);246 const uploadPref = isUnified ? PREF_FHR_UPLOAD_ENABLED : PREF_ENABLED;247 Preferences.set(uploadPref, false);248 // If we're using unified telemetry, disabling ping upload will generate a "deletion"249 // ping. Catch it.250 if (isUnified) {251 let ping = yield PingServer.promiseNextPing();252 checkPingFormat(ping, DELETION_PING_TYPE, true, false);253 }254 // Register a new Ping Handler that asserts if a ping is received, then send a ping.255 PingServer.registerPingHandler(() => Assert.ok(false, "Telemetry must not send pings if not allowed to."));256 let pingId = yield sendPing(true, true);257 // Check that the ping was archived, even with upload disabled.258 let ping = yield TelemetryArchive.promiseArchivedPingById(pingId);259 Assert.equal(ping.id, pingId, "TelemetryController should still archive pings.");260 // Check that pings don't get archived if not allowed to.261 now = new Date(2010, 10, 18, 12, 0, 0);262 fakeNow(now);263 Preferences.set(PREF_ARCHIVE_ENABLED, false);264 pingId = yield sendPing(true, true);265 let promise = TelemetryArchive.promiseArchivedPingById(pingId);266 Assert.ok((yield promiseRejects(promise)),267 "TelemetryController should not archive pings if the archive pref is disabled.");268 // Enable archiving and the upload so that pings get sent and archived again.269 Preferences.set(uploadPref, true);270 Preferences.set(PREF_ARCHIVE_ENABLED, true);271 now = new Date(2014, 6, 18, 22, 0, 0);272 fakeNow(now);273 // Restore the non asserting ping handler.274 PingServer.resetPingHandler();275 pingId = yield sendPing(true, true);276 // Check that we archive pings when successfully sending them.277 yield PingServer.promiseNextPing();278 ping = yield TelemetryArchive.promiseArchivedPingById(pingId);279 Assert.equal(ping.id, pingId,280 "TelemetryController should still archive pings if ping upload is enabled.");281});282// Test that we fuzz the submission time around midnight properly283// to avoid overloading the telemetry servers.284add_task(function* test_midnightPingSendFuzzing() {285 const fuzzingDelay = 60 * 60 * 1000;286 fakeMidnightPingFuzzingDelay(fuzzingDelay);287 let now = new Date(2030, 5, 1, 11, 0, 0);288 fakeNow(now);289 let waitForTimer = () => new Promise(resolve => {290 fakePingSendTimer((callback, timeout) => {291 resolve([callback, timeout]);292 }, () => {});293 });294 PingServer.clearRequests();295 yield TelemetryController.testReset();296 // A ping after midnight within the fuzzing delay should not get sent.297 now = new Date(2030, 5, 2, 0, 40, 0);298 fakeNow(now);299 PingServer.registerPingHandler((req, res) => {300 Assert.ok(false, "No ping should be received yet.");301 });302 let timerPromise = waitForTimer();303 yield sendPing(true, true);304 let [timerCallback, timerTimeout] = yield timerPromise;305 Assert.ok(!!timerCallback);306 Assert.deepEqual(futureDate(now, timerTimeout), new Date(2030, 5, 2, 1, 0, 0));307 // A ping just before the end of the fuzzing delay should not get sent.308 now = new Date(2030, 5, 2, 0, 59, 59);309 fakeNow(now);310 timerPromise = waitForTimer();311 yield sendPing(true, true);312 [timerCallback, timerTimeout] = yield timerPromise;313 Assert.deepEqual(timerTimeout, 1 * 1000);314 // Restore the previous ping handler.315 PingServer.resetPingHandler();316 // Setting the clock to after the fuzzing delay, we should trigger the two ping sends317 // with the timer callback.318 now = futureDate(now, timerTimeout);319 fakeNow(now);320 yield timerCallback();321 const pings = yield PingServer.promiseNextPings(2);322 for (let ping of pings) {323 checkPingFormat(ping, TEST_PING_TYPE, true, true);324 }325 yield TelemetrySend.testWaitOnOutgoingPings();326 // Moving the clock further we should still send pings immediately.327 now = futureDate(now, 5 * 60 * 1000);328 yield sendPing(true, true);329 let ping = yield PingServer.promiseNextPing();330 checkPingFormat(ping, TEST_PING_TYPE, true, true);331 yield TelemetrySend.testWaitOnOutgoingPings();332 // Check that pings shortly before midnight are immediately sent.333 now = fakeNow(2030, 5, 3, 23, 59, 0);334 yield sendPing(true, true);335 ping = yield PingServer.promiseNextPing();336 checkPingFormat(ping, TEST_PING_TYPE, true, true);337 yield TelemetrySend.testWaitOnOutgoingPings();338 // Clean-up.339 fakeMidnightPingFuzzingDelay(0);340 fakePingSendTimer(() => {}, () => {});341});342add_task(function* test_changePingAfterSubmission() {343 // Submit a ping with a custom payload.344 let payload = { canary: "test" };345 let pingPromise = TelemetryController.submitExternalPing(TEST_PING_TYPE, payload, options);346 // Change the payload with a predefined value.347 payload.canary = "changed";348 // Wait for the ping to be archived.349 const pingId = yield pingPromise;350 // Make sure our changes didn't affect the submitted payload.351 let archivedCopy = yield TelemetryArchive.promiseArchivedPingById(pingId);352 Assert.equal(archivedCopy.payload.canary, "test",353 "The payload must not be changed after being submitted.");354});355add_task(function* test_telemetryEnabledUnexpectedValue() {356 // Remove the default value for toolkit.telemetry.enabled from the default prefs.357 // Otherwise, we wouldn't be able to set the pref to a string.358 let defaultPrefBranch = Services.prefs.getDefaultBranch(null);359 defaultPrefBranch.deleteBranch(PREF_ENABLED);360 // Set the preferences controlling the Telemetry status to a string.361 Preferences.set(PREF_ENABLED, "false");362 // Check that Telemetry is not enabled.363 yield TelemetryController.testReset();364 Assert.equal(Telemetry.canRecordExtended, false,365 "Invalid values must not enable Telemetry recording.");366 // Delete the pref again.367 defaultPrefBranch.deleteBranch(PREF_ENABLED);368 // Make sure that flipping it to true works.369 Preferences.set(PREF_ENABLED, true);370 yield TelemetryController.testReset();371 Assert.equal(Telemetry.canRecordExtended, true,372 "True must enable Telemetry recording.");373 // Also check that the false works as well.374 Preferences.set(PREF_ENABLED, false);375 yield TelemetryController.testReset();376 Assert.equal(Telemetry.canRecordExtended, false,377 "False must disable Telemetry recording.");378});379add_task(function* test_telemetryCleanFHRDatabase() {380 const FHR_DBNAME_PREF = "datareporting.healthreport.dbName";381 const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite";382 const DEFAULT_DB_NAME = "healthreport.sqlite";383 // Check that we're able to remove a FHR DB with a custom name.384 const CUSTOM_DB_PATHS = [385 OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME),386 OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-wal"),387 OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-shm"),388 ];389 Preferences.set(FHR_DBNAME_PREF, CUSTOM_DB_NAME);390 // Write fake DB files to the profile directory.391 for (let dbFilePath of CUSTOM_DB_PATHS) {392 yield OS.File.writeAtomic(dbFilePath, "some data");393 }394 // Trigger the cleanup and check that the files were removed.395 yield TelemetryStorage.removeFHRDatabase();396 for (let dbFilePath of CUSTOM_DB_PATHS) {397 Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath);398 }399 // We should not break anything if there's no DB file.400 yield TelemetryStorage.removeFHRDatabase();401 // Check that we're able to remove a FHR DB with the default name.402 Preferences.reset(FHR_DBNAME_PREF);403 const DEFAULT_DB_PATHS = [404 OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME),405 OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-wal"),406 OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-shm"),407 ];408 // Write fake DB files to the profile directory.409 for (let dbFilePath of DEFAULT_DB_PATHS) {410 yield OS.File.writeAtomic(dbFilePath, "some data");411 }412 // Trigger the cleanup and check that the files were removed.413 yield TelemetryStorage.removeFHRDatabase();414 for (let dbFilePath of DEFAULT_DB_PATHS) {415 Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath);416 }417});418add_task(function* stopServer() {419 yield PingServer.stop();...
test_tcping.py
Source:test_tcping.py
...15 tcp_ping = ping.TCPing(destination=args.destination,16 port=args.port,17 timeout=args.timeout,18 use_ipv6=args.use_ipv6)19 tcp_ping.do_ping()20 self.assertEqual(len(tcp_ping.measures), 1)21 def test_ping_domain_incorrect(self):22 with mock.patch('socket.socket') as mock_socket:23 mock_socket.return_value.recv.return_value = b""24 cmd_parser = tcping.create_cmd_parser()25 args = cmd_parser.parse_args(['google.csom'])26 tcp_ping = ping.TCPing(destination=args.destination,27 port=args.port,28 timeout=args.timeout,29 use_ipv6=args.use_ipv6)30 with self.assertRaises(errors.InvalidIpOrDomain):31 tcp_ping.do_ping()32 def test_ping_ip_standart(self):33 with mock.patch('socket.socket') as mock_socket:34 mock_socket.return_value.recv.return_value = b""35 cmd_parser = tcping.create_cmd_parser()36 args = cmd_parser.parse_args(['64.233.165.101', '-c', '10'])37 tcp_ping = ping.TCPing(destination=args.destination,38 port=args.port,39 timeout=args.timeout,40 use_ipv6=args.use_ipv6)41 tcp_ping.do_ping()42 self.assertEqual(len(tcp_ping.measures), 1)43 def test_ping_ip_incorrect(self):44 with mock.patch('socket.socket') as mock_socket:45 mock_socket.return_value.recv.return_value = b""46 cmd_parser = tcping.create_cmd_parser()47 args = cmd_parser.parse_args(['64.233.165.101.123.214'])48 tcp_ping = ping.TCPing(destination=args.destination,49 port=args.port,50 timeout=args.timeout,51 use_ipv6=args.use_ipv6)52 with self.assertRaises(errors.InvalidIpOrDomain):53 tcp_ping.do_ping()54class TestParsing(unittest.TestCase):55 def test_parsing_with_args(self):56 cmd_parser = tcping.create_cmd_parser()57 args = cmd_parser.parse_args(['google.com',58 '-t', '10',59 '-p', '443',60 '-6'])61 tcp_ping = ping.TCPing(destination=args.destination, port=args.port,62 timeout=args.timeout, use_ipv6=args.use_ipv6)63 self.assertEqual(tcp_ping.use_ipv6, True)64 self.assertEqual(tcp_ping.timeout, 10)65 self.assertEqual(tcp_ping.port, 443)66 def test_parsing_without_args_from_cmd(self):67 cmd_parser = tcping.create_cmd_parser()...
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!