TelemetryStorage.jsm 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883
  1. /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. this.EXPORTED_SYMBOLS = ["TelemetryStorage"];
  7. const Cc = Components.classes;
  8. const Ci = Components.interfaces;
  9. const Cr = Components.results;
  10. const Cu = Components.utils;
  11. Cu.import("resource://gre/modules/AppConstants.jsm", this);
  12. Cu.import("resource://gre/modules/Log.jsm");
  13. Cu.import("resource://gre/modules/Services.jsm", this);
  14. Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
  15. Cu.import("resource://gre/modules/osfile.jsm", this);
  16. Cu.import("resource://gre/modules/Task.jsm", this);
  17. Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
  18. Cu.import("resource://gre/modules/Promise.jsm", this);
  19. Cu.import("resource://gre/modules/Preferences.jsm", this);
  20. const LOGGER_NAME = "Toolkit.Telemetry";
  21. const LOGGER_PREFIX = "TelemetryStorage::";
  22. const Telemetry = Services.telemetry;
  23. const Utils = TelemetryUtils;
  24. // Compute the path of the pings archive on the first use.
  25. const DATAREPORTING_DIR = "datareporting";
  26. const PINGS_ARCHIVE_DIR = "archived";
  27. const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
  28. const DELETION_PING_FILE_NAME = "pending-deletion-ping";
  29. const SESSION_STATE_FILE_NAME = "session-state.json";
  30. XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() {
  31. return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
  32. });
  33. XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
  34. return OS.Path.join(gDataReportingDir, PINGS_ARCHIVE_DIR);
  35. });
  36. XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() {
  37. return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME);
  38. });
  39. XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() {
  40. return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME);
  41. });
  42. XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
  43. "resource://services-common/utils.js");
  44. // Maxmimum time, in milliseconds, archive pings should be retained.
  45. const MAX_ARCHIVED_PINGS_RETENTION_MS = 60 * 24 * 60 * 60 * 1000; // 60 days
  46. // Maximum space the archive can take on disk (in Bytes).
  47. const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB
  48. // Maximum space the outgoing pings can take on disk, for Desktop (in Bytes).
  49. const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB
  50. // Maximum space the outgoing pings can take on disk, for Mobile (in Bytes).
  51. const PENDING_PINGS_QUOTA_BYTES_MOBILE = 1024 * 1024; // 1 MB
  52. // The maximum size a pending/archived ping can take on disk.
  53. const PING_FILE_MAXIMUM_SIZE_BYTES = 1024 * 1024; // 1 MB
  54. // This special value is submitted when the archive is outside of the quota.
  55. const ARCHIVE_SIZE_PROBE_SPECIAL_VALUE = 300;
  56. // This special value is submitted when the pending pings is outside of the quota, as
  57. // we don't know the size of the pings above the quota.
  58. const PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE = 17;
  59. const 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 ping
  62. * from the disk fails.
  63. */
  64. function 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. }
  72. PingReadError.prototype = Object.create(Error.prototype);
  73. PingReadError.prototype.constructor = PingReadError;
  74. /**
  75. * This is thrown by |TelemetryStorage.loadPingFile| when parsing the ping JSON
  76. * content fails.
  77. */
  78. function 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. }
  85. PingParseError.prototype = Object.create(Error.prototype);
  86. PingParseError.prototype.constructor = PingParseError;
  87. /**
  88. * This is a policy object used to override behavior for testing.
  89. */
  90. var Policy = {
  91. now: () => new Date(),
  92. getArchiveQuota: () => ARCHIVE_QUOTA_BYTES,
  93. getPendingPingsQuota: () => (AppConstants.platform in ["android"])
  94. ? PENDING_PINGS_QUOTA_BYTES_MOBILE
  95. : PENDING_PINGS_QUOTA_BYTES_DESKTOP,
  96. };
  97. /**
  98. * Wait for all promises in iterable to resolve or reject. This function
  99. * always resolves its promise with undefined, and never rejects.
  100. */
  101. function 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.type
  108. * strings that can be excessively duplicated in the _archivedPings map. Do not
  109. * pass large or temporary strings to this function.
  110. */
  111. function internString(str) {
  112. return Symbol.keyFor(Symbol.for(str));
  113. }
  114. this.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 existing
  153. * 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 so
  273. * 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 if
  326. * 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 overwritten
  337. * 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 saved
  345. * 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 ping
  355. *
  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 the
  372. * 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 in
  379. * 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 particularly
  417. * useful to serialise write access to the disk in order to prevent race conditions
  418. * to corrupt the data being written.
  419. * We are using this to synchronize saving to the file that TelemetrySession persists
  420. * its state in.
  421. */
  422. function SaveSerializer() {
  423. this._queuedOperations = [];
  424. this._queuedInProgress = false;
  425. this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
  426. }
  427. SaveSerializer.prototype = {
  428. /**
  429. * Enqueues an operation to a list to serialise their execution in order to prevent race
  430. * 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. };
  495. var 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 archived
  507. _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 soon
  539. // 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 shutdown
  562. // blockers in place, we a) have seen weird errors being reported that might
  563. // indicate a bad shutdown path and b) might have completion handlers hanging
  564. // 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 it
  772. // 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 if
  810. // 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 pingList
  831. // 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 ping
  842. // 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 its
  873. // 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 if
  929. // 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 ping
  948. // 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 its
  979. // 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 show
  985. // 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 existing
  1002. * 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 if
  1085. * the file exists.
  1086. * @param {bool} [compress=false] If |true|, the file will use lz4 compression. Otherwise no
  1087. * 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 overwritten
  1110. * 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 saved
  1121. * 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 ping
  1132. *
  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 promises
  1219. * are resolved.
  1220. */
  1221. promisePendingPingSaves: function () {
  1222. // Make sure to wait for all the promises, even if they reject. We don't need to log
  1223. // 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 expecting
  1250. // 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 the
  1371. * 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 in
  1525. * 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 one
  1540. // 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 functions
  1556. function 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. }
  1561. function 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. */
  1577. function 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. Note
  1581. // 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.json
  1585. 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. */
  1592. var 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. */
  1607. var 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. */
  1620. function 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-MM
  1627. * format.
  1628. * @return {Object} A Date object or null if the dir name is not valid.
  1629. */
  1630. function 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);
  1639. }