]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/storage.jsm
6cc704f0040c477242a61d3ee7705160621a9961
[dactyl.git] / common / modules / storage.jsm
1 // Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 defineModule("storage", {
8     exports: ["File", "Storage", "storage"],
9     require: ["services", "util"]
10 });
11
12 lazyRequire("config", ["config"]);
13 lazyRequire("io", ["IO"]);
14 lazyRequire("overlay", ["overlay"]);
15
16 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
17 var myObject = JSON.parse("{}").constructor;
18
19 var StoreBase = Class("StoreBase", {
20     OPTIONS: ["privateData", "replacer"],
21
22     fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); },
23
24     get serial() JSON.stringify(this._object, this.replacer),
25
26     init: function (name, store, load, options) {
27         this._load = load;
28         this._options = options;
29
30         this.__defineGetter__("store", () => store);
31         this.__defineGetter__("name", () => name);
32         for (let [k, v] in Iterator(options))
33             if (this.OPTIONS.indexOf(k) >= 0)
34                 this[k] = v;
35         this.reload();
36     },
37
38     clone: function (storage) {
39         let store = storage.privateMode ? false : this.store;
40         let res = this.constructor(this.name, store, this._load, this._options);
41         res.storage = storage;
42         return res;
43     },
44
45     changed: function () { this.timer && this.timer.tell(); },
46
47     reload: function reload() {
48         this._object = this._load() || this._constructor();
49         this.fireEvent("change", null);
50     },
51
52     delete: function delete_() {
53         delete storage.keys[this.name];
54         delete storage[this.name];
55         storage.infoPath.child(this.name).remove(false);
56     },
57
58     save: function () { (self.storage || storage)._saveData(this); },
59
60     __iterator__: function () Iterator(this._object)
61 });
62
63 var ArrayStore = Class("ArrayStore", StoreBase, {
64     _constructor: Array,
65
66     get length() this._object.length,
67
68     set: function set(index, value, quiet) {
69         var orig = this._object[index];
70         this._object[index] = value;
71         if (!quiet)
72             this.fireEvent("change", index);
73
74         return orig;
75     },
76
77     push: function push(value) {
78         this._object.push(value);
79         this.fireEvent("push", this._object.length);
80     },
81
82     pop: function pop(value, ord) {
83         if (ord == null)
84             var res = this._object.pop();
85         else
86             res = this._object.splice(ord, 1)[0];
87
88         this.fireEvent("pop", this._object.length, ord);
89         return res;
90     },
91
92     shift: function shift(value) {
93         var res = this._object.shift();
94         this.fireEvent("shift", this._object.length);
95         return res;
96     },
97
98     insert: function insert(value, ord) {
99         if (ord == 0)
100             this._object.unshift(value);
101         else
102             this._object = this._object.slice(0, ord)
103                                .concat([value])
104                                .concat(this._object.slice(ord));
105         this.fireEvent("insert", this._object.length, ord);
106     },
107
108     truncate: function truncate(length, fromEnd) {
109         var res = this._object.length;
110         if (this._object.length > length) {
111             if (fromEnd)
112                 this._object.splice(0, this._object.length - length);
113             this._object.length = length;
114             this.fireEvent("truncate", length);
115         }
116         return res;
117     },
118
119     // XXX: Awkward.
120     mutate: function mutate(funcName) {
121         var _funcName = funcName;
122         arguments[0] = this._object;
123         this._object = Array[_funcName].apply(Array, arguments);
124         this.fireEvent("change", null);
125     },
126
127     get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index]
128 });
129
130 var ObjectStore = Class("ObjectStore", StoreBase, {
131     _constructor: myObject,
132
133     clear: function () {
134         this._object = {};
135         this.fireEvent("clear");
136     },
137
138     get: function get(key, default_) {
139         return key in this._object  ? this._object[key] :
140                arguments.length > 1 ? this.set(key, default_) :
141                                       undefined;
142     },
143
144     keys: function keys() Object.keys(this._object),
145
146     remove: function remove(key) {
147         var res = this._object[key];
148         delete this._object[key];
149         this.fireEvent("remove", key);
150         return res;
151     },
152
153     set: function set(key, val) {
154         var defined = key in this._object;
155         var orig = this._object[key];
156         this._object[key] = val;
157         if (!defined)
158             this.fireEvent("add", key);
159         else if (orig != val)
160             this.fireEvent("change", key);
161         return val;
162     }
163 });
164
165 var sessionGlobal = Cu.import("resource://gre/modules/Services.jsm", {});
166
167 var Storage = Module("Storage", {
168     Local: function Local(dactyl, modules, window) ({
169         init: function init() {
170             this.privateMode = PrivateBrowsingUtils.isWindowPrivate(window);
171         }
172     }),
173
174     alwaysReload: {},
175
176     init: function init() {
177         this.cleanup();
178
179         let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
180         if (!Services.dactylSession)
181             Services.dactylSession = Cu.createObjectIn(sessionGlobal);
182         this.session = Services.dactylSession;
183     },
184
185     cleanup: function () {
186         this.saveAll();
187
188         for (let key in keys(this.keys)) {
189             if (this[key].timer)
190                 this[key].timer.flush();
191             delete this[key];
192         }
193
194         this.keys = {};
195         this.observers = {};
196     },
197
198     _loadData: function loadData(name, store, type) {
199         try {
200             let file = storage.infoPath.child(name);
201             if (file.exists()) {
202                 let data = file.read();
203                 let result = JSON.parse(data);
204                 if (result instanceof type)
205                     return result;
206             }
207         }
208         catch (e) {
209             util.reportError(e);
210         }
211     },
212
213     _saveData: function saveData(obj) {
214         if (obj.privateData && storage.privateMode)
215             return;
216         if (obj.store && storage.infoPath)
217             storage.infoPath.child(obj.name).write(obj.serial);
218     },
219
220     storeForSession: function storeForSession(key, val) {
221         if (val)
222             this.session[key] = sessionGlobal.JSON.parse(JSON.stringify(val));
223         else
224             delete this.dactylSession[key];
225     },
226
227     infoPath: Class.Memoize(() =>
228         File(IO.runtimePath.replace(/,.*/, ""))
229             .child("info").child(config.profileName)),
230
231     exists: function exists(key) this.infoPath.child(key).exists(),
232
233     remove: function remove(key) {
234         if (this.exists(key)) {
235             if (this[key] && this[key].timer)
236                 this[key].timer.flush();
237             delete this[key];
238             delete this.keys[key];
239             this.infoPath.child(key).remove(false);
240         }
241     },
242
243     newObject: function newObject(key, constructor, params) {
244         if (params == null || !isObject(params))
245             throw Error("Invalid argument type");
246
247         if (this.isLocalModule) {
248             this.globalInstance.newObject.apply(this.globalInstance, arguments);
249
250             if (!(key in this.keys) && this.privateMode && key in this.globalInstance.keys) {
251                 let obj = this.globalInstance.keys[key];
252                 this.keys[key] = this._privatize(obj);
253             }
254
255             return this.keys[key];
256         }
257
258         let reload = params.reload || this.alwaysReload[key];
259         if (!(key in this.keys) || reload) {
260             if (key in this && !reload)
261                 throw Error("Cannot add storage key with that name.");
262
263             let load = () => this._loadData(key, params.store, params.type || myObject);
264
265             this.keys[key] = new constructor(key, params.store, load, params);
266             this.keys[key].timer = new Timer(1000, 10000, () => this.save(key));
267             this.__defineGetter__(key, function () this.keys[key]);
268         }
269         return this.keys[key];
270     },
271
272     newMap: function newMap(key, options) {
273         return this.newObject(key, ObjectStore, options);
274     },
275
276     newArray: function newArray(key, options) {
277         return this.newObject(key, ArrayStore, update({ type: Array }, options));
278     },
279
280     addObserver: function addObserver(key, callback, ref) {
281         if (ref) {
282             let refs = overlay.getData(ref, "storage-refs");
283             refs.push(callback);
284             var callbackRef = util.weakReference(callback);
285         }
286         else {
287             callbackRef = { get: function () callback };
288         }
289
290         this.removeDeadObservers();
291
292         if (!(key in this.observers))
293             this.observers[key] = [];
294
295         if (!this.observers[key].some(o => o.callback.get() == callback))
296             this.observers[key].push({ ref: ref && Cu.getWeakReference(ref),
297                                        callback: callbackRef });
298     },
299
300     removeObserver: function (key, callback) {
301         this.removeDeadObservers();
302
303         if (!(key in this.observers))
304             return;
305
306         this.observers[key] = this.observers[key].filter(elem => elem.callback.get() != callback);
307         if (this.observers[key].length == 0)
308             delete obsevers[key];
309     },
310
311     removeDeadObservers: function () {
312         function filter(o) {
313             if (!o.callback.get())
314                 return false;
315
316             let ref = o.ref && o.ref.get();
317             return ref && !ref.closed && overlay.getData(ref, "storage-refs", null);
318         }
319
320         for (let [key, ary] in Iterator(this.observers)) {
321             this.observers[key] = ary = ary.filter(filter);
322             if (!ary.length)
323                 delete this.observers[key];
324         }
325     },
326
327     fireEvent: function fireEvent(key, event, arg) {
328         this.removeDeadObservers();
329
330         if (key in this.observers)
331             // Safe, since we have our own Array object here.
332             for each (let observer in this.observers[key])
333                 observer.callback.get()(key, event, arg);
334
335         if (key in this.keys && this.keys[key].timer)
336             this[key].timer.tell();
337     },
338
339     load: function load(key) {
340         if (this[key].store && this[key].reload)
341             this[key].reload();
342     },
343
344     save: function save(key) {
345         if (this[key])
346             this._saveData(this.keys[key]);
347     },
348
349     saveAll: function storeAll() {
350         for each (let obj in this.keys)
351             this._saveData(obj);
352     },
353
354     _privateMode: false,
355     get privateMode() this._privateMode,
356     set privateMode(enabled) {
357         this._privateMode = Boolean(enabled);
358
359         if (this.isLocalModule) {
360             this.saveAll();
361
362             if (!enabled)
363                 delete this.keys;
364             else {
365                 let { keys } = this;
366                 this.keys = {};
367                 for (let [k, v] in Iterator(keys))
368                     this.keys[k] = this._privatize(v);
369             }
370         }
371         return this._privateMode;
372     },
373
374     _privatize: function privatize(obj) {
375         if (obj.privateData && obj.clone)
376             return obj.clone(this);
377         return obj;
378     },
379 }, {
380     Replacer: {
381         skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
382     }
383 }, {
384     cleanup: function (dactyl, modules, window) {
385         overlay.setData(window, "storage-refs", null);
386         this.removeDeadObservers();
387     }
388 });
389
390 /**
391  * @class File A class to wrap nsIFile objects and simplify operations
392  * thereon.
393  *
394  * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
395  * @param {boolean} checkPWD Whether to allow expansion relative to the
396  *          current directory. @default true
397  * @param {string} charset The charset of the file. @default File.defaultEncoding
398  */
399 var File = Class("File", {
400     init: function (path, checkPWD, charset) {
401         let file = services.File();
402
403         if (charset)
404             this.charset = charset;
405
406         if (path instanceof Ci.nsIFileURL)
407             path = path.file;
408
409         if (path instanceof Ci.nsIFile || path instanceof File)
410             file = path.clone();
411         else if (/file:\/\//.test(path))
412             file = services["file:"].getFileFromURLSpec(path);
413         else {
414             try {
415                 let expandedPath = File.expandPath(path);
416
417                 if (!File.isAbsolutePath(expandedPath) && checkPWD)
418                     file = checkPWD.child(expandedPath);
419                 else
420                     file.initWithPath(expandedPath);
421             }
422             catch (e) {
423                 util.reportError(e);
424                 return File.DoesNotExist(path, e);
425             }
426         }
427         this.file = file.QueryInterface(Ci.nsILocalFile);
428         return this;
429     },
430
431     charset: Class.Memoize(() => File.defaultEncoding),
432
433     /**
434      * @property {nsIFileURL} Returns the nsIFileURL object for this file.
435      */
436     URI: Class.Memoize(function () {
437         let uri = services.io.newFileURI(this.file)
438                           .QueryInterface(Ci.nsIFileURL);
439         uri.QueryInterface(Ci.nsIMutable).mutable = false;
440         return uri;
441     }),
442
443     /**
444      * Iterates over the objects in this directory.
445      */
446     iterDirectory: function iterDirectory() {
447         if (!this.exists())
448             throw Error(_("io.noSuchFile"));
449         if (!this.isDirectory())
450             throw Error(_("io.eNotDir"));
451         for (let file in iter(this.directoryEntries))
452             yield File(file);
453     },
454
455     /**
456      * Returns a new file for the given child of this directory entry.
457      */
458     child: function child() {
459         let f = this.constructor(this);
460         for (let [, name] in Iterator(arguments))
461             for each (let elem in name.split(File.pathSplit))
462                 f.append(elem);
463         return f;
464     },
465
466     /**
467      * Returns an iterator for all lines in a file.
468      */
469     get lines() File.readLines(services.FileInStream(this.file, -1, 0, 0),
470                                this.charset),
471
472     /**
473      * Reads this file's entire contents in "text" mode and returns the
474      * content as a string.
475      *
476      * @param {string} encoding The encoding from which to decode the file.
477      *          @default #charset
478      * @returns {string}
479      */
480     read: function read(encoding) {
481         let ifstream = services.FileInStream(this.file, -1, 0, 0);
482
483         return File.readStream(ifstream, encoding || this.charset);
484     },
485
486     /**
487      * Returns the list of files in this directory.
488      *
489      * @param {boolean} sort Whether to sort the returned directory
490      *     entries.
491      * @returns {[nsIFile]}
492      */
493     readDirectory: function readDirectory(sort) {
494         if (!this.isDirectory())
495             throw Error(_("io.eNotDir"));
496
497         let array = [e for (e in this.iterDirectory())];
498         if (sort)
499             array.sort((a, b) => (b.isDirectory() - a.isDirectory() ||
500                                   String.localeCompare(a.path, b.path)));
501         return array;
502     },
503
504     /**
505      * Returns a new nsIFileURL object for this file.
506      *
507      * @returns {nsIFileURL}
508      */
509     toURI: function toURI() services.io.newFileURI(this.file),
510
511     /**
512      * Writes the string *buf* to this file.
513      *
514      * @param {string} buf The file content.
515      * @param {string|number} mode The file access mode, a bitwise OR of
516      *     the following flags:
517      *       {@link #MODE_RDONLY}:   0x01
518      *       {@link #MODE_WRONLY}:   0x02
519      *       {@link #MODE_RDWR}:     0x04
520      *       {@link #MODE_CREATE}:   0x08
521      *       {@link #MODE_APPEND}:   0x10
522      *       {@link #MODE_TRUNCATE}: 0x20
523      *       {@link #MODE_SYNC}:     0x40
524      *     Alternatively, the following abbreviations may be used:
525      *       ">"  is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE}
526      *       ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND}
527      * @default ">"
528      * @param {number} perms The file mode bits of the created file. This
529      *     is only used when creating a new file and does not change
530      *     permissions if the file exists.
531      * @default 0644
532      * @param {string} encoding The encoding to used to write the file.
533      * @default #charset
534      */
535     write: function write(buf, mode, perms, encoding) {
536         function getStream(defaultChar) {
537             return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
538         }
539         if (buf instanceof File)
540             buf = buf.read();
541
542         if (!encoding)
543             encoding = this.charset;
544
545         if (mode == ">>")
546             mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
547         else if (!mode || mode == ">")
548             mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE;
549
550         if (!perms)
551             perms = octal(644);
552         if (!this.exists()) // OCREAT won't create the directory
553             this.create(this.NORMAL_FILE_TYPE, perms);
554
555         let ofstream = services.FileOutStream(this.file, mode, perms, 0);
556         try {
557             var ocstream = getStream(0);
558             ocstream.writeString(buf);
559         }
560         catch (e if e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) {
561             ocstream.close();
562             ocstream = getStream("?".charCodeAt(0));
563             ocstream.writeString(buf);
564             return false;
565         }
566         finally {
567             try {
568                 ocstream.close();
569             }
570             catch (e) {}
571             ofstream.close();
572         }
573         return true;
574     },
575
576     // Wrapped native methods:
577     copyTo: function copyTo(dir, name)
578         this.file.copyTo(this.constructor(dir).file,
579                          name),
580
581     copyToFollowingLinks: function copyToFollowingLinks(dir, name)
582         this.file.copyToFollowingLinks(this.constructor(dir).file,
583                                        name),
584
585     moveTo: function moveTo(dir, name)
586         this.file.moveTo(this.constructor(dir).file,
587                          name),
588
589     equals: function equals(file)
590         this.file.equals(this.constructor(file).file),
591
592     contains: function contains(dir, recur)
593         this.file.contains(this.constructor(dir).file,
594                            recur),
595
596     getRelativeDescriptor: function getRelativeDescriptor(file)
597         this.file.getRelativeDescriptor(this.constructor(file).file),
598
599     setRelativeDescriptor: function setRelativeDescriptor(file, path)
600         this.file.setRelativeDescriptor(this.constructor(file).file,
601                                         path)
602 }, {
603     /**
604      * @property {number} Open for reading only.
605      * @final
606      */
607     MODE_RDONLY: 0x01,
608
609     /**
610      * @property {number} Open for writing only.
611      * @final
612      */
613     MODE_WRONLY: 0x02,
614
615     /**
616      * @property {number} Open for reading and writing.
617      * @final
618      */
619     MODE_RDWR: 0x04,
620
621     /**
622      * @property {number} If the file does not exist, the file is created.
623      *     If the file exists, this flag has no effect.
624      * @final
625      */
626     MODE_CREATE: 0x08,
627
628     /**
629      * @property {number} The file pointer is set to the end of the file
630      *     prior to each write.
631      * @final
632      */
633     MODE_APPEND: 0x10,
634
635     /**
636      * @property {number} If the file exists, its length is truncated to 0.
637      * @final
638      */
639     MODE_TRUNCATE: 0x20,
640
641     /**
642      * @property {number} If set, each write will wait for both the file
643      *     data and file status to be physically updated.
644      * @final
645      */
646     MODE_SYNC: 0x40,
647
648     /**
649      * @property {number} With MODE_CREATE, if the file does not exist, the
650      *     file is created. If the file already exists, no action and NULL
651      *     is returned.
652      * @final
653      */
654     MODE_EXCL: 0x80,
655
656     /**
657      * @property {string} The current platform's path separator.
658      */
659     PATH_SEP: Class.Memoize(function () {
660         let f = services.directory.get("CurProcD", Ci.nsIFile);
661         f.append("foo");
662         return f.path.substr(f.parent.path.length, 1);
663     }),
664
665     pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
666
667     DoesNotExist: function DoesNotExist(path, error) ({
668         path: path,
669         exists: function () false,
670         __noSuchMethod__: function () { throw error || Error("Does not exist"); }
671     }),
672
673     defaultEncoding: "UTF-8",
674
675     /**
676      * Expands "~" and environment variables in *path*.
677      *
678      * "~" is expanded to to the value of $HOME. On Windows if this is not
679      * set then the following are tried in order:
680      *   $USERPROFILE
681      *   ${HOMDRIVE}$HOMEPATH
682      *
683      * The variable notation is $VAR (terminated by a non-word character)
684      * or ${VAR}. %VAR% is also supported on Windows.
685      *
686      * @param {string} path The unexpanded path string.
687      * @param {boolean} relative Whether the path is relative or absolute.
688      * @returns {string}
689      */
690     expandPath: function expandPath(path, relative) {
691         function getenv(name) services.environment.get(name);
692
693         // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
694         // TODO: Vim does not expand variables set to an empty string (and documents it).
695         // Kris reckons we shouldn't replicate this 'bug'. --djk
696         // TODO: should we be doing this for all paths?
697         function expand(path) path.replace(
698             win32 ? /\$(\w+)\b|\${(\w+)}|%(\w+)%/g
699                   : /\$(\w+)\b|\${(\w+)}/g,
700             (m, n1, n2, n3) => (getenv(n1 || n2 || n3) || m));
701         path = expand(path);
702
703         // expand ~
704         // Yuck.
705         if (!relative && RegExp("~(?:$|[/" + util.regexp.escape(File.PATH_SEP) + "])").test(path)) {
706             // Try $HOME first, on all systems
707             let home = getenv("HOME");
708
709             // Windows has its own idiosyncratic $HOME variables.
710             if (win32 && (!home || !File(home).exists()))
711                 home = getenv("USERPROFILE") ||
712                        getenv("HOMEDRIVE") + getenv("HOMEPATH");
713
714             path = home + path.substr(1);
715         }
716
717         // TODO: Vim expands paths twice, once before checking for ~, once
718         // after, but doesn't document it. Is this just a bug? --Kris
719         path = expand(path);
720         return path.replace("/", File.PATH_SEP, "g");
721     },
722
723     expandPathList: function (list) list.map(this.expandPath),
724
725     readURL: function readURL(url, encoding) {
726         let channel = services.io.newChannel(url, null, null);
727         channel.contentType = "text/plain";
728         return this.readStream(channel.open(), encoding);
729     },
730
731     readStream: function readStream(ifstream, encoding) {
732         try {
733             var icstream = services.CharsetStream(
734                     ifstream, encoding || File.defaultEncoding, 4096, // buffer size
735                     services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
736
737             let buffer = [];
738             let str = {};
739             while (icstream.readString(4096, str) != 0)
740                 buffer.push(str.value);
741             return buffer.join("");
742         }
743         finally {
744             icstream.close();
745             ifstream.close();
746         }
747     },
748
749     readLines: function readLines(ifstream, encoding) {
750         try {
751             var icstream = services.CharsetStream(
752                     ifstream, encoding || File.defaultEncoding, 4096, // buffer size
753                     services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
754
755             var value = {};
756             while (icstream.readLine(value))
757                 yield value.value;
758         }
759         finally {
760             icstream.close();
761             ifstream.close();
762         }
763     },
764
765     isAbsolutePath: function isAbsolutePath(path) {
766         try {
767             services.File().initWithPath(path);
768             return true;
769         }
770         catch (e) {
771             return false;
772         }
773     },
774
775     joinPaths: function joinPaths(head, tail, cwd) {
776         let path = this(head, cwd);
777         try {
778             // FIXME: should only expand environment vars and normalize path separators
779             path.appendRelativePath(this.expandPath(tail, true));
780         }
781         catch (e) {
782             return File.DoesNotExist(e);
783         }
784         return path;
785     },
786
787     replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
788 });
789
790 let (file = services.directory.get("ProfD", Ci.nsIFile)) {
791     Object.keys(file).forEach(function (prop) {
792         if (!(prop in File.prototype)) {
793             let isFunction;
794             try {
795                 isFunction = callable(file[prop]);
796             }
797             catch (e) {}
798
799             if (isFunction)
800                 File.prototype[prop] = util.wrapCallback(function wrapper() this.file[prop].apply(this.file, arguments));
801             else
802                 Object.defineProperty(File.prototype, prop, {
803                     configurable: true,
804                     get: function wrap_get() this.file[prop],
805                     set: function wrap_set(val) { this.file[prop] = val; }
806                 });
807         }
808     });
809     file = null;
810 }
811
812 endModule();
813
814 // catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
815
816 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: