Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ |
michael@0 | 2 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | "use strict"; |
michael@0 | 7 | |
michael@0 | 8 | this.EXPORTED_SYMBOLS = ["TelemetryFile"]; |
michael@0 | 9 | |
michael@0 | 10 | const Cc = Components.classes; |
michael@0 | 11 | const Ci = Components.interfaces; |
michael@0 | 12 | const Cr = Components.results; |
michael@0 | 13 | const Cu = Components.utils; |
michael@0 | 14 | |
michael@0 | 15 | Cu.import("resource://gre/modules/Services.jsm", this); |
michael@0 | 16 | Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
michael@0 | 17 | Cu.import("resource://gre/modules/osfile.jsm", this); |
michael@0 | 18 | Cu.import("resource://gre/modules/Task.jsm", this); |
michael@0 | 19 | Cu.import("resource://gre/modules/Promise.jsm", this); |
michael@0 | 20 | |
michael@0 | 21 | XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated', |
michael@0 | 22 | 'resource://gre/modules/Deprecated.jsm'); |
michael@0 | 23 | |
michael@0 | 24 | const Telemetry = Services.telemetry; |
michael@0 | 25 | |
michael@0 | 26 | // Files that have been lying around for longer than MAX_PING_FILE_AGE are |
michael@0 | 27 | // deleted without being loaded. |
michael@0 | 28 | const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks |
michael@0 | 29 | |
michael@0 | 30 | // Files that are older than OVERDUE_PING_FILE_AGE, but younger than |
michael@0 | 31 | // MAX_PING_FILE_AGE indicate that we need to send all of our pings ASAP. |
michael@0 | 32 | const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week |
michael@0 | 33 | |
michael@0 | 34 | // The number of outstanding saved pings that we have issued loading |
michael@0 | 35 | // requests for. |
michael@0 | 36 | let pingsLoaded = 0; |
michael@0 | 37 | |
michael@0 | 38 | // The number of pings that we have destroyed due to being older |
michael@0 | 39 | // than MAX_PING_FILE_AGE. |
michael@0 | 40 | let pingsDiscarded = 0; |
michael@0 | 41 | |
michael@0 | 42 | // The number of pings that are older than OVERDUE_PING_FILE_AGE |
michael@0 | 43 | // but younger than MAX_PING_FILE_AGE. |
michael@0 | 44 | let pingsOverdue = 0; |
michael@0 | 45 | |
michael@0 | 46 | // Data that has neither been saved nor sent by ping |
michael@0 | 47 | let pendingPings = []; |
michael@0 | 48 | |
michael@0 | 49 | let isPingDirectoryCreated = false; |
michael@0 | 50 | |
michael@0 | 51 | this.TelemetryFile = { |
michael@0 | 52 | |
michael@0 | 53 | get MAX_PING_FILE_AGE() { |
michael@0 | 54 | return MAX_PING_FILE_AGE; |
michael@0 | 55 | }, |
michael@0 | 56 | |
michael@0 | 57 | get OVERDUE_PING_FILE_AGE() { |
michael@0 | 58 | return OVERDUE_PING_FILE_AGE; |
michael@0 | 59 | }, |
michael@0 | 60 | |
michael@0 | 61 | get pingDirectoryPath() { |
michael@0 | 62 | return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings"); |
michael@0 | 63 | }, |
michael@0 | 64 | |
michael@0 | 65 | /** |
michael@0 | 66 | * Save a single ping to a file. |
michael@0 | 67 | * |
michael@0 | 68 | * @param {object} ping The content of the ping to save. |
michael@0 | 69 | * @param {string} file The destination file. |
michael@0 | 70 | * @param {bool} overwrite If |true|, the file will be overwritten if it exists, |
michael@0 | 71 | * if |false| the file will not be overwritten and no error will be reported if |
michael@0 | 72 | * the file exists. |
michael@0 | 73 | * @returns {promise} |
michael@0 | 74 | */ |
michael@0 | 75 | savePingToFile: function(ping, file, overwrite) { |
michael@0 | 76 | return Task.spawn(function*() { |
michael@0 | 77 | try { |
michael@0 | 78 | let pingString = JSON.stringify(ping); |
michael@0 | 79 | yield OS.File.writeAtomic(file, pingString, {tmpPath: file + ".tmp", |
michael@0 | 80 | noOverwrite: !overwrite}); |
michael@0 | 81 | } catch(e if e.becauseExists) { |
michael@0 | 82 | } |
michael@0 | 83 | }) |
michael@0 | 84 | }, |
michael@0 | 85 | |
michael@0 | 86 | /** |
michael@0 | 87 | * Save a ping to its file. |
michael@0 | 88 | * |
michael@0 | 89 | * @param {object} ping The content of the ping to save. |
michael@0 | 90 | * @param {bool} overwrite If |true|, the file will be overwritten |
michael@0 | 91 | * if it exists. |
michael@0 | 92 | * @returns {promise} |
michael@0 | 93 | */ |
michael@0 | 94 | savePing: function(ping, overwrite) { |
michael@0 | 95 | return Task.spawn(function*() { |
michael@0 | 96 | yield getPingDirectory(); |
michael@0 | 97 | let file = pingFilePath(ping); |
michael@0 | 98 | yield this.savePingToFile(ping, file, overwrite); |
michael@0 | 99 | }.bind(this)); |
michael@0 | 100 | }, |
michael@0 | 101 | |
michael@0 | 102 | /** |
michael@0 | 103 | * Save all pending pings. |
michael@0 | 104 | * |
michael@0 | 105 | * @param {object} sessionPing The additional session ping. |
michael@0 | 106 | * @returns {promise} |
michael@0 | 107 | */ |
michael@0 | 108 | savePendingPings: function(sessionPing) { |
michael@0 | 109 | let p = pendingPings.reduce((p, ping) => { |
michael@0 | 110 | // Restore the files with the previous pings if for some reason they have |
michael@0 | 111 | // been deleted, don't overwrite them otherwise. |
michael@0 | 112 | p.push(this.savePing(ping, false)); |
michael@0 | 113 | return p;}, [this.savePing(sessionPing, true)]); |
michael@0 | 114 | |
michael@0 | 115 | pendingPings = []; |
michael@0 | 116 | return Promise.all(p); |
michael@0 | 117 | }, |
michael@0 | 118 | |
michael@0 | 119 | /** |
michael@0 | 120 | * Remove the file for a ping |
michael@0 | 121 | * |
michael@0 | 122 | * @param {object} ping The ping. |
michael@0 | 123 | * @returns {promise} |
michael@0 | 124 | */ |
michael@0 | 125 | cleanupPingFile: function(ping) { |
michael@0 | 126 | return OS.File.remove(pingFilePath(ping)); |
michael@0 | 127 | }, |
michael@0 | 128 | |
michael@0 | 129 | /** |
michael@0 | 130 | * Load all saved pings. |
michael@0 | 131 | * |
michael@0 | 132 | * Once loaded, the saved pings can be accessed (destructively only) |
michael@0 | 133 | * through |popPendingPings|. |
michael@0 | 134 | * |
michael@0 | 135 | * @returns {promise} |
michael@0 | 136 | */ |
michael@0 | 137 | loadSavedPings: function() { |
michael@0 | 138 | return Task.spawn(function*() { |
michael@0 | 139 | let directory = TelemetryFile.pingDirectoryPath; |
michael@0 | 140 | let iter = new OS.File.DirectoryIterator(directory); |
michael@0 | 141 | let exists = yield iter.exists(); |
michael@0 | 142 | |
michael@0 | 143 | if (exists) { |
michael@0 | 144 | let entries = yield iter.nextBatch(); |
michael@0 | 145 | yield iter.close(); |
michael@0 | 146 | |
michael@0 | 147 | let p = [e for (e of entries) if (!e.isDir)]. |
michael@0 | 148 | map((e) => this.loadHistograms(e.path)); |
michael@0 | 149 | |
michael@0 | 150 | yield Promise.all(p); |
michael@0 | 151 | } |
michael@0 | 152 | |
michael@0 | 153 | yield iter.close(); |
michael@0 | 154 | }.bind(this)); |
michael@0 | 155 | }, |
michael@0 | 156 | |
michael@0 | 157 | /** |
michael@0 | 158 | * Load the histograms from a file. |
michael@0 | 159 | * |
michael@0 | 160 | * Once loaded, the saved pings can be accessed (destructively only) |
michael@0 | 161 | * through |popPendingPings|. |
michael@0 | 162 | * |
michael@0 | 163 | * @param {string} file The file to load. |
michael@0 | 164 | * @returns {promise} |
michael@0 | 165 | */ |
michael@0 | 166 | loadHistograms: function loadHistograms(file) { |
michael@0 | 167 | return OS.File.stat(file).then(function(info){ |
michael@0 | 168 | let now = Date.now(); |
michael@0 | 169 | if (now - info.lastModificationDate > MAX_PING_FILE_AGE) { |
michael@0 | 170 | // We haven't had much luck in sending this file; delete it. |
michael@0 | 171 | pingsDiscarded++; |
michael@0 | 172 | return OS.File.remove(file); |
michael@0 | 173 | } |
michael@0 | 174 | |
michael@0 | 175 | // This file is a bit stale, and overdue for sending. |
michael@0 | 176 | if (now - info.lastModificationDate > OVERDUE_PING_FILE_AGE) { |
michael@0 | 177 | pingsOverdue++; |
michael@0 | 178 | } |
michael@0 | 179 | |
michael@0 | 180 | pingsLoaded++; |
michael@0 | 181 | return addToPendingPings(file); |
michael@0 | 182 | }); |
michael@0 | 183 | }, |
michael@0 | 184 | |
michael@0 | 185 | /** |
michael@0 | 186 | * The number of pings loaded since the beginning of time. |
michael@0 | 187 | */ |
michael@0 | 188 | get pingsLoaded() { |
michael@0 | 189 | return pingsLoaded; |
michael@0 | 190 | }, |
michael@0 | 191 | |
michael@0 | 192 | /** |
michael@0 | 193 | * The number of pings loaded that are older than OVERDUE_PING_FILE_AGE |
michael@0 | 194 | * but younger than MAX_PING_FILE_AGE. |
michael@0 | 195 | */ |
michael@0 | 196 | get pingsOverdue() { |
michael@0 | 197 | return pingsOverdue; |
michael@0 | 198 | }, |
michael@0 | 199 | |
michael@0 | 200 | /** |
michael@0 | 201 | * The number of pings that we just tossed out for being older than |
michael@0 | 202 | * MAX_PING_FILE_AGE. |
michael@0 | 203 | */ |
michael@0 | 204 | get pingsDiscarded() { |
michael@0 | 205 | return pingsDiscarded; |
michael@0 | 206 | }, |
michael@0 | 207 | |
michael@0 | 208 | /** |
michael@0 | 209 | * Iterate destructively through the pending pings. |
michael@0 | 210 | * |
michael@0 | 211 | * @return {iterator} |
michael@0 | 212 | */ |
michael@0 | 213 | popPendingPings: function*(reason) { |
michael@0 | 214 | while (pendingPings.length > 0) { |
michael@0 | 215 | let data = pendingPings.pop(); |
michael@0 | 216 | // Send persisted pings to the test URL too. |
michael@0 | 217 | if (reason == "test-ping") { |
michael@0 | 218 | data.reason = reason; |
michael@0 | 219 | } |
michael@0 | 220 | yield data; |
michael@0 | 221 | } |
michael@0 | 222 | }, |
michael@0 | 223 | |
michael@0 | 224 | testLoadHistograms: function(file) { |
michael@0 | 225 | pingsLoaded = 0; |
michael@0 | 226 | return this.loadHistograms(file.path); |
michael@0 | 227 | } |
michael@0 | 228 | }; |
michael@0 | 229 | |
michael@0 | 230 | ///// Utility functions |
michael@0 | 231 | function pingFilePath(ping) { |
michael@0 | 232 | return OS.Path.join(TelemetryFile.pingDirectoryPath, ping.slug); |
michael@0 | 233 | } |
michael@0 | 234 | |
michael@0 | 235 | function getPingDirectory() { |
michael@0 | 236 | return Task.spawn(function*() { |
michael@0 | 237 | let directory = TelemetryFile.pingDirectoryPath; |
michael@0 | 238 | |
michael@0 | 239 | if (!isPingDirectoryCreated) { |
michael@0 | 240 | yield OS.File.makeDir(directory, { unixMode: OS.Constants.S_IRWXU }); |
michael@0 | 241 | isPingDirectoryCreated = true; |
michael@0 | 242 | } |
michael@0 | 243 | |
michael@0 | 244 | return directory; |
michael@0 | 245 | }); |
michael@0 | 246 | } |
michael@0 | 247 | |
michael@0 | 248 | function addToPendingPings(file) { |
michael@0 | 249 | function onLoad(success) { |
michael@0 | 250 | let success_histogram = Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS"); |
michael@0 | 251 | success_histogram.add(success); |
michael@0 | 252 | } |
michael@0 | 253 | |
michael@0 | 254 | return Task.spawn(function*() { |
michael@0 | 255 | try { |
michael@0 | 256 | let array = yield OS.File.read(file); |
michael@0 | 257 | let decoder = new TextDecoder(); |
michael@0 | 258 | let string = decoder.decode(array); |
michael@0 | 259 | |
michael@0 | 260 | let ping = JSON.parse(string); |
michael@0 | 261 | // The ping's payload used to be stringified JSON. Deal with that. |
michael@0 | 262 | if (typeof(ping.payload) == "string") { |
michael@0 | 263 | ping.payload = JSON.parse(ping.payload); |
michael@0 | 264 | } |
michael@0 | 265 | |
michael@0 | 266 | pendingPings.push(ping); |
michael@0 | 267 | onLoad(true); |
michael@0 | 268 | } catch (e) { |
michael@0 | 269 | onLoad(false); |
michael@0 | 270 | yield OS.File.remove(file); |
michael@0 | 271 | } |
michael@0 | 272 | }); |
michael@0 | 273 | } |