|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["SessionFile"]; |
|
8 |
|
9 /** |
|
10 * Implementation of all the disk I/O required by the session store. |
|
11 * This is a private API, meant to be used only by the session store. |
|
12 * It will change. Do not use it for any other purpose. |
|
13 * |
|
14 * Note that this module implicitly depends on one of two things: |
|
15 * 1. either the asynchronous file I/O system enqueues its requests |
|
16 * and never attempts to simultaneously execute two I/O requests on |
|
17 * the files used by this module from two distinct threads; or |
|
18 * 2. the clients of this API are well-behaved and do not place |
|
19 * concurrent requests to the files used by this module. |
|
20 * |
|
21 * Otherwise, we could encounter bugs, especially under Windows, |
|
22 * e.g. if a request attempts to write sessionstore.js while |
|
23 * another attempts to copy that file. |
|
24 * |
|
25 * This implementation uses OS.File, which guarantees property 1. |
|
26 */ |
|
27 |
|
28 const Cu = Components.utils; |
|
29 const Cc = Components.classes; |
|
30 const Ci = Components.interfaces; |
|
31 const Cr = Components.results; |
|
32 |
|
33 Cu.import("resource://gre/modules/Services.jsm"); |
|
34 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
35 Cu.import("resource://gre/modules/osfile.jsm"); |
|
36 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); |
|
37 Cu.import("resource://gre/modules/Promise.jsm"); |
|
38 Cu.import("resource://gre/modules/AsyncShutdown.jsm"); |
|
39 |
|
40 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
41 "resource://gre/modules/devtools/Console.jsm"); |
|
42 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", |
|
43 "resource://gre/modules/TelemetryStopwatch.jsm"); |
|
44 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
45 "resource://gre/modules/Task.jsm"); |
|
46 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", |
|
47 "@mozilla.org/base/telemetry;1", "nsITelemetry"); |
|
48 |
|
49 this.SessionFile = { |
|
50 /** |
|
51 * Read the contents of the session file, asynchronously. |
|
52 */ |
|
53 read: function () { |
|
54 return SessionFileInternal.read(); |
|
55 }, |
|
56 /** |
|
57 * Write the contents of the session file, asynchronously. |
|
58 */ |
|
59 write: function (aData) { |
|
60 return SessionFileInternal.write(aData); |
|
61 }, |
|
62 /** |
|
63 * Gather telemetry statistics. |
|
64 * |
|
65 * |
|
66 * Most of the work is done off the main thread but there is a main |
|
67 * thread cost involved to send data to the worker thread. This method |
|
68 * should therefore be called only when we know that it will not disrupt |
|
69 * the user's experience, e.g. on idle-daily. |
|
70 * |
|
71 * @return {Promise} |
|
72 * @promise {object} An object holding all the information to be submitted |
|
73 * to Telemetry. |
|
74 */ |
|
75 gatherTelemetry: function(aData) { |
|
76 return SessionFileInternal.gatherTelemetry(aData); |
|
77 }, |
|
78 /** |
|
79 * Create a backup copy, asynchronously. |
|
80 * This is designed to perform backup on upgrade. |
|
81 */ |
|
82 createBackupCopy: function (ext) { |
|
83 return SessionFileInternal.createBackupCopy(ext); |
|
84 }, |
|
85 /** |
|
86 * Remove a backup copy, asynchronously. |
|
87 * This is designed to clean up a backup on upgrade. |
|
88 */ |
|
89 removeBackupCopy: function (ext) { |
|
90 return SessionFileInternal.removeBackupCopy(ext); |
|
91 }, |
|
92 /** |
|
93 * Wipe the contents of the session file, asynchronously. |
|
94 */ |
|
95 wipe: function () { |
|
96 SessionFileInternal.wipe(); |
|
97 } |
|
98 }; |
|
99 |
|
100 Object.freeze(SessionFile); |
|
101 |
|
102 /** |
|
103 * Utilities for dealing with promises and Task.jsm |
|
104 */ |
|
105 let SessionFileInternal = { |
|
106 /** |
|
107 * The path to sessionstore.js |
|
108 */ |
|
109 path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), |
|
110 |
|
111 /** |
|
112 * The path to sessionstore.bak |
|
113 */ |
|
114 backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), |
|
115 |
|
116 /** |
|
117 * The promise returned by the latest call to |write|. |
|
118 * We use it to ensure that AsyncShutdown.profileBeforeChange cannot |
|
119 * interrupt a call to |write|. |
|
120 */ |
|
121 _latestWrite: null, |
|
122 |
|
123 /** |
|
124 * |true| once we have decided to stop receiving write instructiosn |
|
125 */ |
|
126 _isClosed: false, |
|
127 |
|
128 read: function () { |
|
129 // We must initialize the worker during startup so it will be ready to |
|
130 // perform the final write. If shutdown happens soon after startup and |
|
131 // the worker has not started yet we may not write. |
|
132 // See Bug 964531. |
|
133 SessionWorker.post("init"); |
|
134 |
|
135 return Task.spawn(function*() { |
|
136 for (let filename of [this.path, this.backupPath]) { |
|
137 try { |
|
138 let startMs = Date.now(); |
|
139 |
|
140 let data = yield OS.File.read(filename, { encoding: "utf-8" }); |
|
141 |
|
142 Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS") |
|
143 .add(Date.now() - startMs); |
|
144 |
|
145 return data; |
|
146 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { |
|
147 // Ignore exceptions about non-existent files. |
|
148 } |
|
149 } |
|
150 |
|
151 return ""; |
|
152 }.bind(this)); |
|
153 }, |
|
154 |
|
155 gatherTelemetry: function(aStateString) { |
|
156 return Task.spawn(function() { |
|
157 let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]); |
|
158 this._recordTelemetry(msg.telemetry); |
|
159 throw new Task.Result(msg.telemetry); |
|
160 }.bind(this)); |
|
161 }, |
|
162 |
|
163 write: function (aData) { |
|
164 if (this._isClosed) { |
|
165 return Promise.reject(new Error("SessionFile is closed")); |
|
166 } |
|
167 let refObj = {}; |
|
168 |
|
169 let isFinalWrite = false; |
|
170 if (Services.startup.shuttingDown) { |
|
171 // If shutdown has started, we will want to stop receiving |
|
172 // write instructions. |
|
173 isFinalWrite = this._isClosed = true; |
|
174 } |
|
175 |
|
176 return this._latestWrite = Task.spawn(function task() { |
|
177 TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); |
|
178 |
|
179 try { |
|
180 let promise = SessionWorker.post("write", [aData]); |
|
181 // At this point, we measure how long we stop the main thread |
|
182 TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); |
|
183 |
|
184 // Now wait for the result and record how long the write took |
|
185 let msg = yield promise; |
|
186 this._recordTelemetry(msg.telemetry); |
|
187 } catch (ex) { |
|
188 TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); |
|
189 console.error("Could not write session state file ", this.path, ex); |
|
190 } |
|
191 |
|
192 if (isFinalWrite) { |
|
193 Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", ""); |
|
194 } |
|
195 }.bind(this)); |
|
196 }, |
|
197 |
|
198 createBackupCopy: function (ext) { |
|
199 return SessionWorker.post("createBackupCopy", [ext]); |
|
200 }, |
|
201 |
|
202 removeBackupCopy: function (ext) { |
|
203 return SessionWorker.post("removeBackupCopy", [ext]); |
|
204 }, |
|
205 |
|
206 wipe: function () { |
|
207 SessionWorker.post("wipe"); |
|
208 }, |
|
209 |
|
210 _recordTelemetry: function(telemetry) { |
|
211 for (let id of Object.keys(telemetry)){ |
|
212 let value = telemetry[id]; |
|
213 let samples = []; |
|
214 if (Array.isArray(value)) { |
|
215 samples.push(...value); |
|
216 } else { |
|
217 samples.push(value); |
|
218 } |
|
219 let histogram = Telemetry.getHistogramById(id); |
|
220 for (let sample of samples) { |
|
221 histogram.add(sample); |
|
222 } |
|
223 } |
|
224 } |
|
225 }; |
|
226 |
|
227 // Interface to a dedicated thread handling I/O |
|
228 let SessionWorker = (function () { |
|
229 let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js", |
|
230 OS.Shared.LOG.bind("SessionWorker")); |
|
231 return { |
|
232 post: function post(...args) { |
|
233 let promise = worker.post.apply(worker, args); |
|
234 return promise.then( |
|
235 null, |
|
236 function onError(error) { |
|
237 // Decode any serialized error |
|
238 if (error instanceof PromiseWorker.WorkerError) { |
|
239 throw OS.File.Error.fromMsg(error.data); |
|
240 } |
|
241 // Extract something meaningful from ErrorEvent |
|
242 if (error instanceof ErrorEvent) { |
|
243 throw new Error(error.message, error.filename, error.lineno); |
|
244 } |
|
245 throw error; |
|
246 } |
|
247 ); |
|
248 } |
|
249 }; |
|
250 })(); |
|
251 |
|
252 // Ensure that we can write sessionstore.js cleanly before the profile |
|
253 // becomes unaccessible. |
|
254 AsyncShutdown.profileBeforeChange.addBlocker( |
|
255 "SessionFile: Finish writing the latest sessionstore.js", |
|
256 function() { |
|
257 return SessionFileInternal._latestWrite; |
|
258 }); |