gecko/toolkit/components/telemetry/TelemetryFile.jsm

287 lines
8.1 KiB
JavaScript

"use strict";
this.EXPORTED_SYMBOLS = ["TelemetryFile"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
let imports = {};
Cu.import("resource://gre/modules/Services.jsm", imports);
Cu.import("resource://gre/modules/Deprecated.jsm", imports);
Cu.import("resource://gre/modules/NetUtil.jsm", imports);
let {Services, Deprecated, NetUtil} = imports;
// Constants from prio.h for nsIFileOutputStream.init
const PR_WRONLY = 0x2;
const PR_CREATE_FILE = 0x8;
const PR_TRUNCATE = 0x20;
const PR_EXCL = 0x80;
const RW_OWNER = parseInt("0600", 8);
const RWX_OWNER = parseInt("0700", 8);
// Delete ping files that have been lying around for longer than this.
const MAX_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
// The number of outstanding saved pings that we have issued loading
// requests for.
let pingsLoaded = 0;
// The number of those requests that have actually completed.
let pingLoadsCompleted = 0;
// If |true|, send notifications "telemetry-test-save-complete"
// and "telemetry-test-load-complete" once save/load is complete.
let shouldNotifyUponSave = false;
// Data that has neither been saved nor sent by ping
let pendingPings = [];
this.TelemetryFile = {
/**
* Save a single ping to a file.
*
* @param {object} ping The content of the ping to save.
* @param {nsIFile} file The destination file.
* @param {bool} sync If |true|, write synchronously. Deprecated.
* This argument should be |false|.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
*/
savePingToFile: function(ping, file, sync, overwrite) {
let pingString = JSON.stringify(ping);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let ostream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
let initFlags = PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE;
if (!overwrite) {
initFlags |= PR_EXCL;
}
try {
ostream.init(file, initFlags, RW_OWNER, 0);
} catch (e) {
// Probably due to PR_EXCL.
return;
}
if (sync) {
let utf8String = converter.ConvertFromUnicode(pingString);
utf8String += converter.Finish();
let success = false;
try {
let amount = ostream.write(utf8String, utf8String.length);
success = amount == utf8String.length;
} catch (e) {
}
finishTelemetrySave(success, ostream);
} else {
let istream = converter.convertToInputStream(pingString);
let self = this;
NetUtil.asyncCopy(istream, ostream,
function(result) {
finishTelemetrySave(Components.isSuccessCode(result),
ostream);
});
}
},
/**
* Save a ping to its file, synchronously.
*
* @param {object} ping The content of the ping to save.
* @param {bool} overwrite If |true|, the file will be overwritten
* if it exists.
*/
savePing: function(ping, overwrite) {
this.savePingToFile(ping,
getSaveFileForPing(ping), true, overwrite);
},
/**
* Save all pending pings, synchronously.
*
* @param {object} sessionPing The additional session ping.
*/
savePendingPings: function(sessionPing) {
this.savePing(sessionPing, true);
pendingPings.forEach(function sppcb(e, i, a) {
this.savePing(e, false);
}, this);
pendingPings = [];
},
/**
* Remove the file for a ping
*
* @param {object} ping The ping.
*/
cleanupPingFile: function(ping) {
// FIXME: We shouldn't create the directory just to remove the file.
let file = getSaveFileForPing(ping);
try {
file.remove(true); // FIXME: Should be |false|, isn't it?
} catch(e) {
}
},
/**
* Load all saved pings.
*
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {bool} sync If |true|, loading takes place synchronously.
* @param {function*} onLoad A function called upon loading of each
* ping. It is passed |true| in case of success, |false| in case of
* format error.
*/
loadSavedPings: function(sync, onLoad = null) {
let directory = ensurePingDirectory();
let entries = directory.directoryEntries
.QueryInterface(Ci.nsIDirectoryEnumerator);
pingsLoaded = 0;
pingLoadsCompleted = 0;
try {
while (entries.hasMoreElements()) {
this.loadHistograms(entries.nextFile, sync, onLoad);
}
} finally {
entries.close();
}
},
/**
* Load the histograms from a file.
*
* Once loaded, the saved pings can be accessed (destructively only)
* through |popPendingPings|.
*
* @param {nsIFile} file The file to load.
* @param {bool} sync If |true|, loading takes place synchronously.
* @param {function*} onLoad A function called upon loading of the
* ping. It is passed |true| in case of success, |false| in case of
* format error.
*/
loadHistograms: function loadHistograms(file, sync, onLoad = null) {
let now = new Date();
if (now - file.lastModifiedTime > MAX_PING_FILE_AGE) {
// We haven't had much luck in sending this file; delete it.
file.remove(true);
return;
}
pingsLoaded++;
if (sync) {
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
stream.init(file, -1, -1, 0);
addToPendingPings(file, stream, onLoad);
} else {
let channel = NetUtil.newChannel(file);
channel.contentType = "application/json";
NetUtil.asyncFetch(channel, (function(stream, result) {
if (!Components.isSuccessCode(result)) {
return;
}
addToPendingPings(file, stream, onLoad);
}).bind(this));
}
},
/**
* The number of pings loaded since the beginning of time.
*/
get pingsLoaded() {
return pingsLoaded;
},
/**
* Iterate destructively through the pending pings.
*
* @return {iterator}
*/
popPendingPings: function(reason) {
while (pendingPings.length > 0) {
let data = pendingPings.pop();
// Send persisted pings to the test URL too.
if (reason == "test-ping") {
data.reason = reason;
}
yield data;
}
},
set shouldNotifyUponSave(value) {
shouldNotifyUponSave = value;
},
testLoadHistograms: function(file, sync, onLoad) {
pingsLoaded = 0;
pingLoadsCompleted = 0;
this.loadHistograms(file, sync, onLoad);
}
};
///// Utility functions
function getSaveFileForPing(ping) {
let file = ensurePingDirectory();
file.append(ping.slug);
return file;
};
function ensurePingDirectory() {
let directory = Services.dirsvc.get("ProfD", Ci.nsILocalFile).clone();
directory.append("saved-telemetry-pings");
try {
directory.create(Ci.nsIFile.DIRECTORY_TYPE, RWX_OWNER);
} catch (e) {
// Already exists, just ignore this.
}
return directory;
};
function addToPendingPings(file, stream, onLoad) {
let success = false;
try {
let string = NetUtil.readInputStreamToString(stream, stream.available(),
{ charset: "UTF-8" });
stream.close();
let ping = JSON.parse(string);
// The ping's payload used to be stringified JSON. Deal with that.
if (typeof(ping.payload) == "string") {
ping.payload = JSON.parse(ping.payload);
}
pingLoadsCompleted++;
pendingPings.push(ping);
if (shouldNotifyUponSave &&
pingLoadsCompleted == pingsLoaded) {
Services.obs.notifyObservers(null, "telemetry-test-load-complete", null);
}
success = true;
} catch (e) {
// An error reading the file, or an error parsing the contents.
stream.close(); // close is idempotent.
file.remove(true); // FIXME: Should be false, isn't it?
}
if (onLoad) {
onLoad(success);
}
};
function finishTelemetrySave(ok, stream) {
stream.close();
if (shouldNotifyUponSave && ok) {
Services.obs.notifyObservers(null, "telemetry-test-save-complete", null);
}
};