summaryrefslogtreecommitdiffstats
path: root/webbrowser/components/sessionstore/_SessionFile.jsm
blob: 62b4d1687bfaa8d23bd561d648a0c6f16859edec (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

this.EXPORTED_SYMBOLS = ["_SessionFile"];

/**
 * Implementation of all the disk I/O required by the session store.
 * This is a private API, meant to be used only by the session store.
 * It will change. Do not use it for any other purpose.
 *
 * Note that this module implicitly depends on one of two things:
 * 1. either the asynchronous file I/O system enqueues its requests
 *   and never attempts to simultaneously execute two I/O requests on
 *   the files used by this module from two distinct threads; or
 * 2. the clients of this API are well-behaved and do not place
 *   concurrent requests to the files used by this module.
 *
 * Otherwise, we could encounter bugs, especially under Windows,
 *   e.g. if a request attempts to write sessionstore.js while
 *   another attempts to copy that file.
 *
 * This implementation uses OS.File, which guarantees property 1.
 */

const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Promise.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
  "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
  "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
  "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "console",
  "resource://gre/modules/Console.jsm");

// An encoder to UTF-8.
XPCOMUtils.defineLazyGetter(this, "gEncoder", function () {
  return new TextEncoder();
});
// A decoder.
XPCOMUtils.defineLazyGetter(this, "gDecoder", function () {
  return new TextDecoder();
});

this._SessionFile = {
  /**
   * A promise fulfilled once initialization (either synchronous or
   * asynchronous) is complete.
   */
  promiseInitialized: function SessionFile_initialized() {
    return SessionFileInternal.promiseInitialized;
  },
  /**
   * Read the contents of the session file, asynchronously.
   */
  read: function SessionFile_read() {
    return SessionFileInternal.read();
  },
  /**
   * Read the contents of the session file, synchronously.
   */
  syncRead: function SessionFile_syncRead() {
    return SessionFileInternal.syncRead();
  },
  /**
   * Write the contents of the session file, asynchronously.
   */
  write: function SessionFile_write(aData) {
    return SessionFileInternal.write(aData);
  },
  /**
   * Create a backup copy, asynchronously.
   */
  createBackupCopy: function SessionFile_createBackupCopy() {
    return SessionFileInternal.createBackupCopy();
  },
  /**
   * Wipe the contents of the session file, asynchronously.
   */
  wipe: function SessionFile_wipe() {
    return SessionFileInternal.wipe();
  }
};

Object.freeze(_SessionFile);

/**
 * Utilities for dealing with promises and Task.jsm
 */
const TaskUtils = {
  /**
   * Add logging to a promise.
   *
   * @param {Promise} promise
   * @return {Promise} A promise behaving as |promise|, but with additional
   * logging in case of uncaught error.
   */
  captureErrors: function captureErrors(promise) {
    return promise.then(
      null,
      function onError(reason) {
        console.error("Uncaught asynchronous error:", reason);
        throw reason;
      }
    );
  },
  /**
   * Spawn a new Task from a generator.
   *
   * This function behaves as |Task.spawn|, with the exception that it
   * adds logging in case of uncaught error. For more information, see
   * the documentation of |Task.jsm|.
   *
   * @param {generator} gen Some generator.
   * @return {Promise} A promise built from |gen|, with the same semantics
   * as |Task.spawn(gen)|.
   */
  spawn: function spawn(gen) {
    return this.captureErrors(Task.spawn(gen));
  }
};

var SessionFileInternal = {
  /**
   * A promise fulfilled once initialization is complete
   */
  promiseInitialized: Promise.defer(),

  /**
   * The path to sessionstore.js
   */
  path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),

  /**
   * The path to sessionstore.bak
   */
  backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),

  /**
   * Utility function to safely read a file synchronously.
   * @param aPath
   *        A path to read the file from.
   * @returns string if successful, undefined otherwise.
   */
  readAuxSync: function ssfi_readAuxSync(aPath) {
    let text;
    try {
      let file = new FileUtils.File(aPath);
      let chan = NetUtil.newChannel({
        uri: NetUtil.newURI(file),
        loadUsingSystemPrincipal: true
      });
      let stream = chan.open();
      text = NetUtil.readInputStreamToString(stream, stream.available(),
        {charset: "utf-8"});
    } catch (e if e.result == Components.results.NS_ERROR_FILE_NOT_FOUND) {
      // Ignore exceptions about non-existent files.
    } catch (ex) {
      // Any other error.
      console.error("Uncaught error:", ex);
    } finally {
      return text;
    }
  },

  /**
   * Read the sessionstore file synchronously.
   *
   * This function is meant to serve as a fallback in case of race
   * between a synchronous usage of the API and asynchronous
   * initialization.
   *
   * In case if sessionstore.js file does not exist or is corrupted (something
   * happened between backup and write), attempt to read the sessionstore.bak
   * instead.
   */
  syncRead: function ssfi_syncRead() {
    // First read the sessionstore.js.
    let text = this.readAuxSync(this.path);
    if (typeof text === "undefined") {
      // If sessionstore.js does not exist or is corrupted, read sessionstore.bak.
      text = this.readAuxSync(this.backupPath);
    }
    return text || "";
  },

  /**
   * Utility function to safely read a file asynchronously.
   * @param aPath
   *        A path to read the file from.
   * @param aReadOptions
   *        Read operation options.
   *        |outExecutionDuration| option will be reused and can be
   *        incrementally updated by the worker process.
   * @returns string if successful, undefined otherwise.
   */
  readAux: function ssfi_readAux(aPath, aReadOptions) {
    let self = this;
    return TaskUtils.spawn(function () {
      let text;
      try {
        let bytes = yield OS.File.read(aPath, undefined, aReadOptions);
        text = gDecoder.decode(bytes);
      } catch (ex if self._isNoSuchFile(ex)) {
        // Ignore exceptions about non-existent files.
      } catch (ex) {
        // Any other error.
        console.error("Uncaught error - with the file: " + self.path, ex);
      }
      throw new Task.Result(text);
    });
  },

  /**
   * Read the sessionstore file asynchronously.
   *
   * In case sessionstore.js file does not exist or is corrupted (something
   * happened between backup and write), attempt to read the sessionstore.bak
   * instead.
   */
  read: function ssfi_read() {
    let self = this;
    return TaskUtils.spawn(function task() {
      // Specify |outExecutionDuration| option to hold the combined duration of
      // the asynchronous reads off the main thread (of both sessionstore.js and
      // sessionstore.bak, if necessary). If sessionstore.js does not exist or
      // is corrupted, |outExecutionDuration| will register the time it took to
      // attempt to read the file. It will then be subsequently incremented by
      // the read time of sessionsore.bak.
      let readOptions = {
        outExecutionDuration: null
      };
      // First read the sessionstore.js.
      let text = yield self.readAux(self.path, readOptions);
      if (typeof text === "undefined") {
        // If sessionstore.js does not exist or is corrupted, read the
        // sessionstore.bak.
        text = yield self.readAux(self.backupPath, readOptions);
      }
      // Return either the content of the sessionstore.bak if it was read
      // successfully or an empty string otherwise.
      throw new Task.Result(text || "");
    });
  },

  write: function ssfi_write(aData) {
    let refObj = {};
    let self = this;
    return TaskUtils.spawn(function task() {
      let bytes = gEncoder.encode(aData);

      try {
        let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
        yield promise;
      } catch (ex) {
        console.error("Could not write session state file: " + self.path, ex);
      }
    });
  },

  createBackupCopy: function ssfi_createBackupCopy() {
    let backupCopyOptions = {
      outExecutionDuration: null
    };
    let self = this;
    return TaskUtils.spawn(function task() {
      try {
        yield OS.File.move(self.path, self.backupPath, backupCopyOptions);
      } catch (ex if self._isNoSuchFile(ex)) {
        // Ignore exceptions about non-existent files.
      } catch (ex) {
        console.error("Could not backup session state file: " + self.path, ex);
        throw ex;
      }
    });
  },

  wipe: function ssfi_wipe() {
    let self = this;
    return TaskUtils.spawn(function task() {
      try {
        yield OS.File.remove(self.path);
      } catch (ex if self._isNoSuchFile(ex)) {
        // Ignore exceptions about non-existent files.
      } catch (ex) {
        console.error("Could not remove session state file: " + self.path, ex);
        throw ex;
      }

      try {
        yield OS.File.remove(self.backupPath);
      } catch (ex if self._isNoSuchFile(ex)) {
        // Ignore exceptions about non-existent files.
      } catch (ex) {
        console.error("Could not remove session state backup file: " + self.path, ex);
        throw ex;
      }
    });
  },

  _isNoSuchFile: function ssfi_isNoSuchFile(aReason) {
    return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile;
  }
};