OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 // Shared cloud importer namespace | 5 // Shared cloud importer namespace |
6 var importer = importer || {}; | 6 var importer = importer || {}; |
7 | 7 |
8 /** @enum {string} */ | 8 /** @enum {string} */ |
9 importer.ScanEvent = { | 9 importer.ScanEvent = { |
10 FINALIZED: 'finalized', | 10 FINALIZED: 'finalized', |
11 INVALIDATED: 'invalidated', | 11 INVALIDATED: 'invalidated', |
12 UPDATED: 'updated' | 12 UPDATED: 'updated' |
13 }; | 13 }; |
14 | 14 |
15 /** | 15 /** |
16 * Storage keys for settings saved by importer. | 16 * Storage keys for settings saved by importer. |
17 * @enum {string} | 17 * @enum {string} |
18 */ | 18 */ |
19 importer.Setting = { | 19 importer.Setting = { |
20 HAS_COMPLETED_IMPORT: 'importer-has-completed-import', | 20 HAS_COMPLETED_IMPORT: 'importer-has-completed-import', |
21 MACHINE_ID: 'importer-machine-id', | 21 MACHINE_ID: 'importer-machine-id', |
22 PHOTOS_APP_ENABLED: 'importer-photo-app-enabled' | 22 PHOTOS_APP_ENABLED: 'importer-photo-app-enabled', |
| 23 LAST_KNOWN_LOG_ID: 'importer-last-known-log-id' |
23 }; | 24 }; |
24 | 25 |
25 /** | 26 /** |
26 * @typedef {function( | 27 * @typedef {function( |
27 * !importer.ScanEvent, importer.ScanResult)} | 28 * !importer.ScanEvent, importer.ScanResult)} |
28 */ | 29 */ |
29 importer.ScanObserver; | 30 importer.ScanObserver; |
30 | 31 |
31 /** | 32 /** |
32 * Volume types eligible for the affections of Cloud Import. | 33 * Volume types eligible for the affections of Cloud Import. |
(...skipping 183 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
216 }); | 217 }); |
217 }; | 218 }; |
218 | 219 |
219 /** | 220 /** |
220 * @return {!Promise.<string>} Resolves with the filename of this | 221 * @return {!Promise.<string>} Resolves with the filename of this |
221 * machines history file. | 222 * machines history file. |
222 */ | 223 */ |
223 importer.getHistoryFilename = function() { | 224 importer.getHistoryFilename = function() { |
224 return importer.getMachineId().then( | 225 return importer.getMachineId().then( |
225 function(machineId) { | 226 function(machineId) { |
226 return 'import-history.' + machineId + '.log'; | 227 return machineId + '-import-history.log'; |
227 }); | 228 }); |
228 }; | 229 }; |
229 | 230 |
230 /** | 231 /** |
| 232 * @param {number} logId |
231 * @return {!Promise.<string>} Resolves with the filename of this | 233 * @return {!Promise.<string>} Resolves with the filename of this |
232 * machines debug log file. | 234 * machines debug log file. |
233 */ | 235 */ |
234 importer.getDebugLogFilename = function() { | 236 importer.getDebugLogFilename = function(logId) { |
235 return importer.getMachineId().then( | 237 return importer.getMachineId().then( |
236 function(machineId) { | 238 function(machineId) { |
237 return 'import-debug.' + machineId + '.log'; | 239 return machineId + '-import-debug-' + logId + '.log'; |
238 }); | 240 }); |
239 }; | 241 }; |
240 | 242 |
241 /** | 243 /** |
242 * @return {number} A relatively unique six digit integer that is most likely | 244 * @return {number} A relatively unique six digit integer that is most likely |
243 * unique to this machine among a user's machines. Used only to segregate | 245 * unique to this machine among a user's machines. Used only to segregate |
244 * log files on sync storage. | 246 * log files on sync storage. |
245 */ | 247 */ |
246 importer.generateMachineId_ = function() { | 248 importer.generateMachineId_ = function() { |
247 return Math.floor(Math.random() * 899999) + 100000; | 249 return Math.floor(Math.random() * 899999) + 100000; |
(...skipping 388 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
636 | 638 |
637 /** | 639 /** |
638 * A {@code importer.Logger} that persists data in a {@code FileEntry}. | 640 * A {@code importer.Logger} that persists data in a {@code FileEntry}. |
639 * | 641 * |
640 * @constructor | 642 * @constructor |
641 * @implements {importer.Logger} | 643 * @implements {importer.Logger} |
642 * @struct | 644 * @struct |
643 * @final | 645 * @final |
644 * | 646 * |
645 * @param {!Promise.<!FileEntry>} fileEntryPromise | 647 * @param {!Promise.<!FileEntry>} fileEntryPromise |
| 648 * @param {!Promise.<!analytics.Tracker>} trackerPromise |
646 */ | 649 */ |
647 importer.RuntimeLogger = function(fileEntryPromise) { | 650 importer.RuntimeLogger = function(fileEntryPromise, trackerPromise) { |
648 | 651 |
649 /** @private {!Promise.<!importer.PromisingFileEntry>} */ | 652 /** @private {!Promise.<!importer.PromisingFileEntry>} */ |
650 this.fileEntryPromise_ = fileEntryPromise.then( | 653 this.fileEntryPromise_ = fileEntryPromise.then( |
651 /** @param {!FileEntry} fileEntry */ | 654 /** @param {!FileEntry} fileEntry */ |
652 function(fileEntry) { | 655 function(fileEntry) { |
653 return new importer.PromisingFileEntry(fileEntry); | 656 return new importer.PromisingFileEntry(fileEntry); |
654 }); | 657 }); |
| 658 |
| 659 /** @private {!Promise.<!analytics.Tracker>} */ |
| 660 this.trackerPromise_ = trackerPromise; |
| 661 }; |
| 662 |
| 663 /** |
| 664 * Reports an error to analytics. |
| 665 * |
| 666 * @param {string} context MUST NOT contain any dynamic error content, |
| 667 * only statically defined string will dooooo. |
| 668 */ |
| 669 importer.RuntimeLogger.prototype.reportErrorContext_ = function(context) { |
| 670 this.trackerPromise_.then( |
| 671 /** @param {!analytics.Tracker} tracker */ |
| 672 function(tracker) { |
| 673 tracker.sendException( |
| 674 context, |
| 675 false /* fatal */ ); |
| 676 }); |
655 }; | 677 }; |
656 | 678 |
657 /** @override */ | 679 /** @override */ |
658 importer.RuntimeLogger.prototype.info = function(content) { | 680 importer.RuntimeLogger.prototype.info = function(content) { |
659 this.write_('INFO', content); | 681 this.write_('INFO', content); |
660 console.log(content); | 682 console.log(content); |
661 }; | 683 }; |
662 | 684 |
663 /** @override */ | 685 /** @override */ |
664 importer.RuntimeLogger.prototype.error = function(content) { | 686 importer.RuntimeLogger.prototype.error = function(content) { |
665 this.write_('ERROR', content); | 687 this.write_('ERROR', content); |
666 console.error(content); | 688 console.error(content); |
667 }; | 689 }; |
668 | 690 |
669 /** @override */ | 691 /** @override */ |
670 importer.RuntimeLogger.prototype.catcher = function(context) { | 692 importer.RuntimeLogger.prototype.catcher = function(context) { |
| 693 var prefix = '(' + context + ') '; |
671 return function(error) { | 694 return function(error) { |
672 this.error('Caught promise error. Context: ' + context + | 695 this.reportErrorContext_(context); |
673 ' Error: ' + error.message); | 696 |
| 697 var message = prefix + 'Caught error in promise chain.'; |
| 698 if (error) { |
| 699 // Error can be anything...maybe an Error, maybe a string. |
| 700 var error = error.message || error; |
| 701 this.error(message + ' Error: ' + error); |
| 702 if (error.stack) { |
| 703 this.write_('STACK', prefix + error.stack); |
| 704 } |
| 705 } else { |
| 706 this.error(message); |
| 707 error = new Error(message); |
| 708 } |
| 709 |
| 710 throw error; |
674 }.bind(this); | 711 }.bind(this); |
675 }; | 712 }; |
676 | 713 |
677 /** | 714 /** |
678 * Writes a message to the logger followed by a new line. | 715 * Writes a message to the logger followed by a new line. |
679 * | 716 * |
680 * @param {string} type | 717 * @param {string} type |
681 * @param {string} message | 718 * @param {string} message |
682 */ | 719 */ |
683 importer.RuntimeLogger.prototype.write_ = function(type, message) { | 720 importer.RuntimeLogger.prototype.write_ = function(type, message) { |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
721 /** @private {importer.Logger} */ | 758 /** @private {importer.Logger} */ |
722 importer.logger_ = null; | 759 importer.logger_ = null; |
723 | 760 |
724 /** | 761 /** |
725 * Creates a new logger instance...all ready to go. | 762 * Creates a new logger instance...all ready to go. |
726 * | 763 * |
727 * @return {!importer.Logger} | 764 * @return {!importer.Logger} |
728 */ | 765 */ |
729 importer.getLogger = function() { | 766 importer.getLogger = function() { |
730 if (!importer.logger_) { | 767 if (!importer.logger_) { |
| 768 var nextLogId = importer.getNextDebugLogId_(); |
| 769 |
| 770 /** @return {!Promise} */ |
| 771 var rotator = function() { |
| 772 return importer.rotateLogs( |
| 773 nextLogId, |
| 774 importer.ChromeSyncFileEntryProvider.getFileEntry); |
| 775 }; |
| 776 |
| 777 // This is a sligtly odd arrangement in service of two goals. |
| 778 // |
| 779 // 1) Make a logger available synchronously. |
| 780 // 2) Nuke old log files before reusing their names. |
| 781 // |
| 782 // In support of these goals we push the "rotator" between |
| 783 // the call to load the file entry and the method that |
| 784 // produces the name of the file to load. That method |
| 785 // (getDebugLogFilename) returns promise. We exploit this. |
731 importer.logger_ = new importer.RuntimeLogger( | 786 importer.logger_ = new importer.RuntimeLogger( |
732 importer.ChromeSyncFileEntryProvider.getFileEntry( | 787 importer.ChromeSyncFileEntryProvider.getFileEntry( |
733 importer.getDebugLogFilename())); | 788 /** @type {!Promise.<string>} */ (rotator().then( |
| 789 importer.getDebugLogFilename.bind(null, nextLogId)))), |
| 790 importer.getTracker_()); |
734 } | 791 } |
| 792 |
735 return importer.logger_; | 793 return importer.logger_; |
736 }; | 794 }; |
737 | 795 |
738 /** | 796 /** |
| 797 * Fetch analytics.Tracker from background page. |
| 798 * @return {!Promise.<!analytics.Tracker>} |
| 799 * @private |
| 800 */ |
| 801 importer.getTracker_ = function() { |
| 802 return new Promise( |
| 803 function(resolve, reject) { |
| 804 chrome.runtime.getBackgroundPage( |
| 805 /** @param {Window=} opt_background */ |
| 806 function(opt_background) { |
| 807 if (chrome.runtime.lastError) { |
| 808 reject(chrome.runtime.lastError); |
| 809 } |
| 810 opt_background.background.ready( |
| 811 function() { |
| 812 resolve(opt_background.background.tracker); |
| 813 }); |
| 814 }); |
| 815 }); |
| 816 }; |
| 817 |
| 818 /** |
| 819 * Returns the log ID for the next debug log to use. |
| 820 * @private |
| 821 */ |
| 822 importer.getNextDebugLogId_ = function() { |
| 823 // Changes every other month. |
| 824 return new Date().getMonth() % 2; |
| 825 }; |
| 826 |
| 827 /** |
| 828 * Deletes the "next" log file if it has just-now become active. |
| 829 * |
| 830 * Basically we toggle back and forth writing to two log files. At the time |
| 831 * we flip from one to another we want to delete the oldest data we have. |
| 832 * In this case it will be the "next" log. |
| 833 * |
| 834 * This function must be run before instantiating the logger. |
| 835 * |
| 836 * @param {number} nextLogId |
| 837 * @param {function(!Promise<string>): !Promise<!FileEntry>} fileFactory |
| 838 * Injected primarily to facilitate testing. |
| 839 * @return {!Promise} Resolves when trimming is complete. |
| 840 */ |
| 841 importer.rotateLogs = function(nextLogId, fileFactory) { |
| 842 var storage = importer.ChromeLocalStorage.getInstance(); |
| 843 |
| 844 /** @return {!Promise} */ |
| 845 var rememberLogId = function() { |
| 846 return storage.set( |
| 847 importer.Setting.LAST_KNOWN_LOG_ID, |
| 848 nextLogId); |
| 849 }; |
| 850 |
| 851 return storage.get(importer.Setting.LAST_KNOWN_LOG_ID) |
| 852 .then( |
| 853 /** @param {number} lastKnownLogId */ |
| 854 function(lastKnownLogId) { |
| 855 if (nextLogId === lastKnownLogId || |
| 856 lastKnownLogId === undefined) { |
| 857 return Promise.resolve(); |
| 858 } |
| 859 |
| 860 return fileFactory(importer.getDebugLogFilename(nextLogId)) |
| 861 .then( |
| 862 /** |
| 863 * @param {!FileEntry} entry |
| 864 * @return {!Promise} |
| 865 * @suppress {checkTypes} |
| 866 */ |
| 867 function(entry) { |
| 868 return new Promise(entry.remove.bind(entry)); |
| 869 }); |
| 870 }) |
| 871 .then(rememberLogId) |
| 872 .catch(rememberLogId); |
| 873 }; |
| 874 |
| 875 /** |
739 * Friendly wrapper around chrome.storage.local. | 876 * Friendly wrapper around chrome.storage.local. |
740 * | 877 * |
741 * NOTE: If you want to use this in a test, install MockChromeStorageAPI. | 878 * NOTE: If you want to use this in a test, install MockChromeStorageAPI. |
742 * | 879 * |
743 * @constructor | 880 * @constructor |
744 */ | 881 */ |
745 importer.ChromeLocalStorage = function() {}; | 882 importer.ChromeLocalStorage = function() {}; |
746 | 883 |
747 /** | 884 /** |
748 * @param {string} key | 885 * @param {string} key |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
791 }); | 928 }); |
792 }; | 929 }; |
793 | 930 |
794 /** @private @const {!importer.ChromeLocalStorage} */ | 931 /** @private @const {!importer.ChromeLocalStorage} */ |
795 importer.ChromeLocalStorage.INSTANCE_ = new importer.ChromeLocalStorage(); | 932 importer.ChromeLocalStorage.INSTANCE_ = new importer.ChromeLocalStorage(); |
796 | 933 |
797 /** @return {!importer.ChromeLocalStorage} */ | 934 /** @return {!importer.ChromeLocalStorage} */ |
798 importer.ChromeLocalStorage.getInstance = function() { | 935 importer.ChromeLocalStorage.getInstance = function() { |
799 return importer.ChromeLocalStorage.INSTANCE_; | 936 return importer.ChromeLocalStorage.INSTANCE_; |
800 }; | 937 }; |
OLD | NEW |