michael@0: /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["TelemetryFile"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm", this); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: Cu.import("resource://gre/modules/osfile.jsm", this); michael@0: Cu.import("resource://gre/modules/Task.jsm", this); michael@0: Cu.import("resource://gre/modules/Promise.jsm", this); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated', michael@0: 'resource://gre/modules/Deprecated.jsm'); michael@0: michael@0: const Telemetry = Services.telemetry; michael@0: michael@0: // Files that have been lying around for longer than MAX_PING_FILE_AGE are michael@0: // deleted without being loaded. michael@0: const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks michael@0: michael@0: // Files that are older than OVERDUE_PING_FILE_AGE, but younger than michael@0: // MAX_PING_FILE_AGE indicate that we need to send all of our pings ASAP. michael@0: const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week michael@0: michael@0: // The number of outstanding saved pings that we have issued loading michael@0: // requests for. michael@0: let pingsLoaded = 0; michael@0: michael@0: // The number of pings that we have destroyed due to being older michael@0: // than MAX_PING_FILE_AGE. michael@0: let pingsDiscarded = 0; michael@0: michael@0: // The number of pings that are older than OVERDUE_PING_FILE_AGE michael@0: // but younger than MAX_PING_FILE_AGE. michael@0: let pingsOverdue = 0; michael@0: michael@0: // Data that has neither been saved nor sent by ping michael@0: let pendingPings = []; michael@0: michael@0: let isPingDirectoryCreated = false; michael@0: michael@0: this.TelemetryFile = { michael@0: michael@0: get MAX_PING_FILE_AGE() { michael@0: return MAX_PING_FILE_AGE; michael@0: }, michael@0: michael@0: get OVERDUE_PING_FILE_AGE() { michael@0: return OVERDUE_PING_FILE_AGE; michael@0: }, michael@0: michael@0: get pingDirectoryPath() { michael@0: return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings"); michael@0: }, michael@0: michael@0: /** michael@0: * Save a single ping to a file. michael@0: * michael@0: * @param {object} ping The content of the ping to save. michael@0: * @param {string} file The destination file. michael@0: * @param {bool} overwrite If |true|, the file will be overwritten if it exists, michael@0: * if |false| the file will not be overwritten and no error will be reported if michael@0: * the file exists. michael@0: * @returns {promise} michael@0: */ michael@0: savePingToFile: function(ping, file, overwrite) { michael@0: return Task.spawn(function*() { michael@0: try { michael@0: let pingString = JSON.stringify(ping); michael@0: yield OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp", michael@0: noOverwrite: !overwrite}); michael@0: } catch(e if e.becauseExists) { michael@0: } michael@0: }) michael@0: }, michael@0: michael@0: /** michael@0: * Save a ping to its file. michael@0: * michael@0: * @param {object} ping The content of the ping to save. michael@0: * @param {bool} overwrite If |true|, the file will be overwritten michael@0: * if it exists. michael@0: * @returns {promise} michael@0: */ michael@0: savePing: function(ping, overwrite) { michael@0: return Task.spawn(function*() { michael@0: yield getPingDirectory(); michael@0: let file = pingFilePath(ping); michael@0: yield this.savePingToFile(ping, file, overwrite); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Save all pending pings. michael@0: * michael@0: * @param {object} sessionPing The additional session ping. michael@0: * @returns {promise} michael@0: */ michael@0: savePendingPings: function(sessionPing) { michael@0: let p = pendingPings.reduce((p, ping) => { michael@0: // Restore the files with the previous pings if for some reason they have michael@0: // been deleted, don't overwrite them otherwise. michael@0: p.push(this.savePing(ping, false)); michael@0: return p;}, [this.savePing(sessionPing, true)]); michael@0: michael@0: pendingPings = []; michael@0: return Promise.all(p); michael@0: }, michael@0: michael@0: /** michael@0: * Remove the file for a ping michael@0: * michael@0: * @param {object} ping The ping. michael@0: * @returns {promise} michael@0: */ michael@0: cleanupPingFile: function(ping) { michael@0: return OS.File.remove(pingFilePath(ping)); michael@0: }, michael@0: michael@0: /** michael@0: * Load all saved pings. michael@0: * michael@0: * Once loaded, the saved pings can be accessed (destructively only) michael@0: * through |popPendingPings|. michael@0: * michael@0: * @returns {promise} michael@0: */ michael@0: loadSavedPings: function() { michael@0: return Task.spawn(function*() { michael@0: let directory = TelemetryFile.pingDirectoryPath; michael@0: let iter = new OS.File.DirectoryIterator(directory); michael@0: let exists = yield iter.exists(); michael@0: michael@0: if (exists) { michael@0: let entries = yield iter.nextBatch(); michael@0: yield iter.close(); michael@0: michael@0: let p = [e for (e of entries) if (!e.isDir)]. michael@0: map((e) => this.loadHistograms(e.path)); michael@0: michael@0: yield Promise.all(p); michael@0: } michael@0: michael@0: yield iter.close(); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Load the histograms from a file. michael@0: * michael@0: * Once loaded, the saved pings can be accessed (destructively only) michael@0: * through |popPendingPings|. michael@0: * michael@0: * @param {string} file The file to load. michael@0: * @returns {promise} michael@0: */ michael@0: loadHistograms: function loadHistograms(file) { michael@0: return OS.File.stat(file).then(function(info){ michael@0: let now = Date.now(); michael@0: if (now - info.lastModificationDate > MAX_PING_FILE_AGE) { michael@0: // We haven't had much luck in sending this file; delete it. michael@0: pingsDiscarded++; michael@0: return OS.File.remove(file); michael@0: } michael@0: michael@0: // This file is a bit stale, and overdue for sending. michael@0: if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) { michael@0: pingsOverdue++; michael@0: } michael@0: michael@0: pingsLoaded++; michael@0: return addToPendingPings(file); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The number of pings loaded since the beginning of time. michael@0: */ michael@0: get pingsLoaded() { michael@0: return pingsLoaded; michael@0: }, michael@0: michael@0: /** michael@0: * The number of pings loaded that are older than OVERDUE_PING_FILE_AGE michael@0: * but younger than MAX_PING_FILE_AGE. michael@0: */ michael@0: get pingsOverdue() { michael@0: return pingsOverdue; michael@0: }, michael@0: michael@0: /** michael@0: * The number of pings that we just tossed out for being older than michael@0: * MAX_PING_FILE_AGE. michael@0: */ michael@0: get pingsDiscarded() { michael@0: return pingsDiscarded; michael@0: }, michael@0: michael@0: /** michael@0: * Iterate destructively through the pending pings. michael@0: * michael@0: * @return {iterator} michael@0: */ michael@0: popPendingPings: function*(reason) { michael@0: while (pendingPings.length > 0) { michael@0: let data = pendingPings.pop(); michael@0: // Send persisted pings to the test URL too. michael@0: if (reason == "test-ping") { michael@0: data.reason = reason; michael@0: } michael@0: yield data; michael@0: } michael@0: }, michael@0: michael@0: testLoadHistograms: function(file) { michael@0: pingsLoaded = 0; michael@0: return this.loadHistograms(file.path); michael@0: } michael@0: }; michael@0: michael@0: ///// Utility functions michael@0: function pingFilePath(ping) { michael@0: return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug); michael@0: } michael@0: michael@0: function getPingDirectory() { michael@0: return Task.spawn(function*() { michael@0: let directory = TelemetryFile.pingDirectoryPath; michael@0: michael@0: if (!isPingDirectoryCreated) { michael@0: yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU }); michael@0: isPingDirectoryCreated = true; michael@0: } michael@0: michael@0: return directory; michael@0: }); michael@0: } michael@0: michael@0: function addToPendingPings(file) { michael@0: function onLoad(success) { michael@0: let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS"); michael@0: success_histogram.add(success); michael@0: } michael@0: michael@0: return Task.spawn(function*() { michael@0: try { michael@0: let array = yield OS.File.read(file); michael@0: let decoder = new TextDecoder(); michael@0: let string = decoder.decode(array); michael@0: michael@0: let ping = JSON.parse(string); michael@0: // The ping's payload used to be stringified JSON. Deal with that. michael@0: if (typeof(ping.payload) == "string") { michael@0: ping.payload = JSON.parse(ping.payload); michael@0: } michael@0: michael@0: pendingPings.push(ping); michael@0: onLoad(true); michael@0: } catch (e) { michael@0: onLoad(false); michael@0: yield OS.File.remove(file); michael@0: } michael@0: }); michael@0: }