Initial Proper Commit
This commit is contained in:
		
							parent
							
								
									b9ccf7f08e
								
							
						
					
					
						commit
						e3e0bb0b17
					
				
					 40 changed files with 4281 additions and 8 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| node_modules | ||||
| *.asar | ||||
							
								
								
									
										16
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
										
									
									
									
								
							|  | @ -4,15 +4,17 @@ | |||
| ## Goals | ||||
| - **Hotpluggable** - just swap the asar file, nothing else needed | ||||
| - **Lightweight** - it should be at least as fast or lightweight, hopefully more | ||||
| - **No Tracking** - no crash reporting, etc | ||||
| - **No Tracking** - no crash reporting, error tracking, etc | ||||
| - **Minimal** - generally only doing what is needed (see: implementation) | ||||
| 
 | ||||
| ## Implementation | ||||
| Below is a list in order of priority, marked as complete when finished: | ||||
| - [ ] Stub everything | ||||
| - [ ] Bootstrapping | ||||
| - [ ] Splash screen | ||||
| - [X] Bootstrapping | ||||
| - [X] Splash screen | ||||
| - [X] Error handling | ||||
| - [ ] A bunch of specific minor fixes / features | ||||
|   - [ ] Handle hardware acceleration | ||||
| - [ ] Auto start | ||||
| - [ ] Updating | ||||
|   - [ ] Updater v1 | ||||
|   - [ ] Updater v2 | ||||
| - [ ] First run | ||||
| - [ ] Self-write updater code (currently mostly copied) | ||||
| - [ ] Self-write some small parts of internals | ||||
							
								
								
									
										34
									
								
								src/Constants.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/Constants.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| // Bootstrap consts, heavily copied as don't want to mess with it
 | ||||
| const { releaseChannel } = require('./utils/buildInfo'); | ||||
| 
 | ||||
| const { getSettings } = require('./appSettings'); | ||||
| 
 | ||||
| const settings = getSettings(); | ||||
| 
 | ||||
| function capitalizeFirstLetter(s) { | ||||
|   return s.charAt(0).toUpperCase() + s.slice(1); | ||||
| } | ||||
| 
 | ||||
| const appNameSuffix = releaseChannel === 'stable' ? '' : capitalizeFirstLetter(releaseChannel); | ||||
| const APP_COMPANY = 'Discord Inc'; | ||||
| const APP_DESCRIPTION = 'Discord - https://discord.com'; | ||||
| const APP_NAME = 'Discord' + appNameSuffix; | ||||
| const APP_NAME_FOR_HUMANS = 'Discord' + (appNameSuffix !== '' ? ' ' + appNameSuffix : ''); | ||||
| const APP_ID_BASE = 'com.squirrel'; | ||||
| const APP_ID = `${APP_ID_BASE}.${APP_NAME}.${APP_NAME}`; | ||||
| const APP_PROTOCOL = 'Discord'; | ||||
| const API_ENDPOINT = settings.get('API_ENDPOINT') || 'https://discord.com/api'; | ||||
| const UPDATE_ENDPOINT = settings.get('UPDATE_ENDPOINT') || API_ENDPOINT; | ||||
| const NEW_UPDATE_ENDPOINT = settings.get('NEW_UPDATE_ENDPOINT') || 'https://discord.com/api/updates/'; | ||||
| 
 | ||||
| module.exports = { | ||||
|   APP_COMPANY, | ||||
|   APP_DESCRIPTION, | ||||
|   APP_NAME, | ||||
|   APP_NAME_FOR_HUMANS, | ||||
|   APP_ID, | ||||
|   APP_PROTOCOL, | ||||
|   API_ENDPOINT, | ||||
|   NEW_UPDATE_ENDPOINT, | ||||
|   UPDATE_ENDPOINT | ||||
| }; | ||||
							
								
								
									
										6
									
								
								src/GPUSettings.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/GPUSettings.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| // Idk why Discord has to use this
 | ||||
| exports.replace = (GPUSettings) => { | ||||
|   for (const name of Object.keys(GPUSettings)) { | ||||
|     exports[name] = GPUSettings[name]; | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										7
									
								
								src/appSettings.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/appSettings.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| const Settings = require('./utils/Settings'); | ||||
| const paths = require('./paths'); | ||||
| 
 | ||||
| const settings = new Settings(paths.getUserData()); | ||||
| 
 | ||||
| exports.getSettings = () => settings; | ||||
| exports.init = () => {}; // Stub as we setup on require
 | ||||
							
								
								
									
										6
									
								
								src/autoStart/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/autoStart/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| // Stub for now at least
 | ||||
| 
 | ||||
| exports.install = (callback) => { callback(); }; | ||||
| exports.update = (callback) => { callback(); }; | ||||
| exports.uninstall = (callback) => { callback(); }; | ||||
| exports.isInstalled = (callback) => { callback(true); }; // Stub to true or false?
 | ||||
							
								
								
									
										63
									
								
								src/bootstrap.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/bootstrap.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| const { join } = require('path'); | ||||
| const NodeModule = require('module'); | ||||
| const { app } = require('electron'); | ||||
| 
 | ||||
| const log = require('./utils/log'); | ||||
| const requireNative = require('./utils/requireNative'); | ||||
| const paths = require('./paths'); | ||||
| const buildInfo = require('./utils/buildInfo'); | ||||
| 
 | ||||
| // Just required for startup
 | ||||
| const appSettings = require('./appSettings'); | ||||
| const GPUSettings = require('./GPUSettings'); | ||||
| const crashReporterSetup = require('./crashReporterSetup'); | ||||
| const splashScreen = require('./splash/splashScreen'); | ||||
| const Constants = require('./Constants'); | ||||
| const autoStart = require('./autoStart'); | ||||
| 
 | ||||
| const updater = require('./updater/updater'); | ||||
| const moduleUpdater = require('./updater/moduleUpdater'); | ||||
| const appUpdater = require('./updater/appUpdater'); | ||||
| 
 | ||||
| let desktopCore; | ||||
| const startCore = () => { | ||||
|   desktopCore = requireNative('discord_desktop_core'); | ||||
|   log('Bootstrap', 'Required desktop_core:', desktopCore); | ||||
| 
 | ||||
|   desktopCore.startup({ | ||||
|     paths, | ||||
|     splashScreen, | ||||
|     moduleUpdater, | ||||
|     autoStart, | ||||
|     buildInfo, | ||||
|     appSettings, | ||||
|     Constants, | ||||
|     GPUSettings, | ||||
|     updater, | ||||
|     crashReporterSetup | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const startUpdate = () => { | ||||
|   appUpdater.update(false, () => { | ||||
|     startCore(); | ||||
|   }, () => { | ||||
|     desktopCore.setMainWindowVisible(true); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| module.exports = () => { | ||||
|   // Paths logging
 | ||||
|   log('Paths', `Init! Returns:
 | ||||
| getUserData: ${paths.getUserData()} | ||||
| getUserDataVersioned: ${paths.getUserDataVersioned()} | ||||
| getResources: ${paths.getResources()} | ||||
| getModuleDataPath: ${paths.getModuleDataPath()} | ||||
| getInstallPath: ${paths.getInstallPath()}`);
 | ||||
| 
 | ||||
|   if (app.isReady()) { | ||||
|     startUpdate(); | ||||
|   } else { | ||||
|     app.once('ready', startUpdate); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										5
									
								
								src/crashReporterSetup.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/crashReporterSetup.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| // Much crash reporting, such wow
 | ||||
| exports.init = () => {}; | ||||
| exports.isInitialized = () => true; | ||||
| 
 | ||||
| exports.metadata = {}; | ||||
							
								
								
									
										40
									
								
								src/errorHandler.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/errorHandler.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| const { app } = require("electron"); | ||||
| 
 | ||||
| const log = require('./utils/log'); | ||||
| 
 | ||||
| exports.init = () => { | ||||
|   /* process.on('uncaughtException', error => { | ||||
|     const stack = error.stack ? error.stack : String(error); | ||||
|     const message = `Uncaught exception:\n ${stack}`; | ||||
|     console.warn(message); | ||||
| 
 | ||||
|     if (!isErrorSafeToSuppress(error)) { | ||||
|       _electron.dialog.showErrorBox('A JavaScript error occurred in the main process', message); | ||||
|     } | ||||
|   }); */ | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| exports.fatal = (err) => { | ||||
|   const options = { | ||||
|     type: 'error', | ||||
|     message: 'A fatal Javascript error occured', | ||||
|     detail: err && err.stack ? err.stack : String(err) | ||||
|   }; | ||||
| 
 | ||||
|   const callback = _ => app.quit(); | ||||
| 
 | ||||
|   const electronMajor = parseInt(process.versions.electron.split('.')[0]); | ||||
| 
 | ||||
|   if (electronMajor >= 6) { | ||||
|     _electron.dialog.showMessageBox(null, options).then(callback); | ||||
|   } else { | ||||
|     _electron.dialog.showMessageBox(options, callback); | ||||
|   } | ||||
| 
 | ||||
|   log('ErrorHandler', 'Fatal:', err); | ||||
| }; | ||||
| 
 | ||||
| exports.handled = (err) => { | ||||
|   log('ErrorHandler', 'Handled:', err); | ||||
| }; | ||||
							
								
								
									
										3
									
								
								src/firstRun/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/firstRun/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| // Stub for now at least
 | ||||
| 
 | ||||
| exports.update = (callback) => { callback(); }; | ||||
							
								
								
									
										7
									
								
								src/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| const log = require('./utils/log'); | ||||
| 
 | ||||
| log('', 'Initing...'); | ||||
| 
 | ||||
| const bootstrap = require('./bootstrap'); | ||||
| 
 | ||||
| bootstrap(); // Start bootstrap
 | ||||
							
								
								
									
										5
									
								
								src/ipcMain.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/ipcMain.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| // Discord's wrapper around ipcMain
 | ||||
| const { ipcMain } = require('electron'); | ||||
| 
 | ||||
| exports.on = (event, callback) => ipcMain.on('DISCORD_' + event, callback); | ||||
| exports.removeListener = (event, callback) => ipcMain.removeListener('DISCORD_' + event, callback); | ||||
							
								
								
									
										873
									
								
								src/package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										873
									
								
								src/package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,873 @@ | |||
| { | ||||
|   "name": "openasar", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "openasar", | ||||
|       "dependencies": { | ||||
|         "mkdirp": "^0.5.1", | ||||
|         "request": "2.88.0", | ||||
|         "yauzl": "^2.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ajv": { | ||||
|       "version": "6.12.6", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", | ||||
|       "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", | ||||
|       "dependencies": { | ||||
|         "fast-deep-equal": "^3.1.1", | ||||
|         "fast-json-stable-stringify": "^2.0.0", | ||||
|         "json-schema-traverse": "^0.4.1", | ||||
|         "uri-js": "^4.2.2" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "type": "github", | ||||
|         "url": "https://github.com/sponsors/epoberezkin" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/asn1": { | ||||
|       "version": "0.2.6", | ||||
|       "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", | ||||
|       "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", | ||||
|       "dependencies": { | ||||
|         "safer-buffer": "~2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/assert-plus": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", | ||||
|       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", | ||||
|       "engines": { | ||||
|         "node": ">=0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/asynckit": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||
|       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" | ||||
|     }, | ||||
|     "node_modules/aws-sign2": { | ||||
|       "version": "0.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", | ||||
|       "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/aws4": { | ||||
|       "version": "1.11.0", | ||||
|       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", | ||||
|       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" | ||||
|     }, | ||||
|     "node_modules/bcrypt-pbkdf": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", | ||||
|       "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", | ||||
|       "dependencies": { | ||||
|         "tweetnacl": "^0.14.3" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/buffer-crc32": { | ||||
|       "version": "0.2.13", | ||||
|       "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", | ||||
|       "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/caseless": { | ||||
|       "version": "0.12.0", | ||||
|       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", | ||||
|       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" | ||||
|     }, | ||||
|     "node_modules/combined-stream": { | ||||
|       "version": "1.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||
|       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", | ||||
|       "dependencies": { | ||||
|         "delayed-stream": "~1.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/core-util-is": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", | ||||
|       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" | ||||
|     }, | ||||
|     "node_modules/dashdash": { | ||||
|       "version": "1.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", | ||||
|       "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", | ||||
|       "dependencies": { | ||||
|         "assert-plus": "^1.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/delayed-stream": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||
|       "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", | ||||
|       "engines": { | ||||
|         "node": ">=0.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/ecc-jsbn": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", | ||||
|       "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", | ||||
|       "dependencies": { | ||||
|         "jsbn": "~0.1.0", | ||||
|         "safer-buffer": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/extend": { | ||||
|       "version": "3.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", | ||||
|       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" | ||||
|     }, | ||||
|     "node_modules/extsprintf": { | ||||
|       "version": "1.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", | ||||
|       "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", | ||||
|       "engines": [ | ||||
|         "node >=0.6.0" | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/fast-deep-equal": { | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | ||||
|       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" | ||||
|     }, | ||||
|     "node_modules/fast-json-stable-stringify": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", | ||||
|       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" | ||||
|     }, | ||||
|     "node_modules/fd-slicer": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", | ||||
|       "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", | ||||
|       "dependencies": { | ||||
|         "pend": "~1.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/forever-agent": { | ||||
|       "version": "0.6.1", | ||||
|       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", | ||||
|       "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/form-data": { | ||||
|       "version": "2.3.3", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", | ||||
|       "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", | ||||
|       "dependencies": { | ||||
|         "asynckit": "^0.4.0", | ||||
|         "combined-stream": "^1.0.6", | ||||
|         "mime-types": "^2.1.12" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 0.12" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/getpass": { | ||||
|       "version": "0.1.7", | ||||
|       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", | ||||
|       "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", | ||||
|       "dependencies": { | ||||
|         "assert-plus": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/har-schema": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", | ||||
|       "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", | ||||
|       "engines": { | ||||
|         "node": ">=4" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/har-validator": { | ||||
|       "version": "5.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", | ||||
|       "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", | ||||
|       "deprecated": "this library is no longer supported", | ||||
|       "dependencies": { | ||||
|         "ajv": "^6.12.3", | ||||
|         "har-schema": "^2.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/http-signature": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", | ||||
|       "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", | ||||
|       "dependencies": { | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "jsprim": "^1.2.2", | ||||
|         "sshpk": "^1.7.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.8", | ||||
|         "npm": ">=1.3.7" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/is-typedarray": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", | ||||
|       "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" | ||||
|     }, | ||||
|     "node_modules/isstream": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", | ||||
|       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" | ||||
|     }, | ||||
|     "node_modules/jsbn": { | ||||
|       "version": "0.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", | ||||
|       "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" | ||||
|     }, | ||||
|     "node_modules/json-schema": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", | ||||
|       "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" | ||||
|     }, | ||||
|     "node_modules/json-schema-traverse": { | ||||
|       "version": "0.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", | ||||
|       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" | ||||
|     }, | ||||
|     "node_modules/json-stringify-safe": { | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", | ||||
|       "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" | ||||
|     }, | ||||
|     "node_modules/jsprim": { | ||||
|       "version": "1.4.2", | ||||
|       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", | ||||
|       "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", | ||||
|       "dependencies": { | ||||
|         "assert-plus": "1.0.0", | ||||
|         "extsprintf": "1.3.0", | ||||
|         "json-schema": "0.4.0", | ||||
|         "verror": "1.10.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.6.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/mime-db": { | ||||
|       "version": "1.51.0", | ||||
|       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", | ||||
|       "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", | ||||
|       "engines": { | ||||
|         "node": ">= 0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/mime-types": { | ||||
|       "version": "2.1.34", | ||||
|       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", | ||||
|       "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", | ||||
|       "dependencies": { | ||||
|         "mime-db": "1.51.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/minimist": { | ||||
|       "version": "1.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", | ||||
|       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" | ||||
|     }, | ||||
|     "node_modules/mkdirp": { | ||||
|       "version": "0.5.5", | ||||
|       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", | ||||
|       "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", | ||||
|       "dependencies": { | ||||
|         "minimist": "^1.2.5" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "mkdirp": "bin/cmd.js" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/oauth-sign": { | ||||
|       "version": "0.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", | ||||
|       "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/pend": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", | ||||
|       "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" | ||||
|     }, | ||||
|     "node_modules/performance-now": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", | ||||
|       "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" | ||||
|     }, | ||||
|     "node_modules/psl": { | ||||
|       "version": "1.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", | ||||
|       "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" | ||||
|     }, | ||||
|     "node_modules/punycode": { | ||||
|       "version": "1.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", | ||||
|       "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" | ||||
|     }, | ||||
|     "node_modules/qs": { | ||||
|       "version": "6.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", | ||||
|       "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", | ||||
|       "engines": { | ||||
|         "node": ">=0.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/request": { | ||||
|       "version": "2.88.0", | ||||
|       "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", | ||||
|       "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", | ||||
|       "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", | ||||
|       "dependencies": { | ||||
|         "aws-sign2": "~0.7.0", | ||||
|         "aws4": "^1.8.0", | ||||
|         "caseless": "~0.12.0", | ||||
|         "combined-stream": "~1.0.6", | ||||
|         "extend": "~3.0.2", | ||||
|         "forever-agent": "~0.6.1", | ||||
|         "form-data": "~2.3.2", | ||||
|         "har-validator": "~5.1.0", | ||||
|         "http-signature": "~1.2.0", | ||||
|         "is-typedarray": "~1.0.0", | ||||
|         "isstream": "~0.1.2", | ||||
|         "json-stringify-safe": "~5.0.1", | ||||
|         "mime-types": "~2.1.19", | ||||
|         "oauth-sign": "~0.9.0", | ||||
|         "performance-now": "^2.1.0", | ||||
|         "qs": "~6.5.2", | ||||
|         "safe-buffer": "^5.1.2", | ||||
|         "tough-cookie": "~2.4.3", | ||||
|         "tunnel-agent": "^0.6.0", | ||||
|         "uuid": "^3.3.2" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 4" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/safe-buffer": { | ||||
|       "version": "5.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", | ||||
|       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "github", | ||||
|           "url": "https://github.com/sponsors/feross" | ||||
|         }, | ||||
|         { | ||||
|           "type": "patreon", | ||||
|           "url": "https://www.patreon.com/feross" | ||||
|         }, | ||||
|         { | ||||
|           "type": "consulting", | ||||
|           "url": "https://feross.org/support" | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     "node_modules/safer-buffer": { | ||||
|       "version": "2.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", | ||||
|       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" | ||||
|     }, | ||||
|     "node_modules/sshpk": { | ||||
|       "version": "1.16.1", | ||||
|       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", | ||||
|       "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", | ||||
|       "dependencies": { | ||||
|         "asn1": "~0.2.3", | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "bcrypt-pbkdf": "^1.0.0", | ||||
|         "dashdash": "^1.12.0", | ||||
|         "ecc-jsbn": "~0.1.1", | ||||
|         "getpass": "^0.1.1", | ||||
|         "jsbn": "~0.1.0", | ||||
|         "safer-buffer": "^2.0.2", | ||||
|         "tweetnacl": "~0.14.0" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "sshpk-conv": "bin/sshpk-conv", | ||||
|         "sshpk-sign": "bin/sshpk-sign", | ||||
|         "sshpk-verify": "bin/sshpk-verify" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/tough-cookie": { | ||||
|       "version": "2.4.3", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", | ||||
|       "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", | ||||
|       "dependencies": { | ||||
|         "psl": "^1.1.24", | ||||
|         "punycode": "^1.4.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=0.8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/tunnel-agent": { | ||||
|       "version": "0.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", | ||||
|       "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", | ||||
|       "dependencies": { | ||||
|         "safe-buffer": "^5.0.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/tweetnacl": { | ||||
|       "version": "0.14.5", | ||||
|       "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", | ||||
|       "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" | ||||
|     }, | ||||
|     "node_modules/uri-js": { | ||||
|       "version": "4.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", | ||||
|       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", | ||||
|       "dependencies": { | ||||
|         "punycode": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/uri-js/node_modules/punycode": { | ||||
|       "version": "2.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", | ||||
|       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", | ||||
|       "engines": { | ||||
|         "node": ">=6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/uuid": { | ||||
|       "version": "3.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", | ||||
|       "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", | ||||
|       "deprecated": "Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.", | ||||
|       "bin": { | ||||
|         "uuid": "bin/uuid" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/verror": { | ||||
|       "version": "1.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", | ||||
|       "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", | ||||
|       "engines": [ | ||||
|         "node >=0.6.0" | ||||
|       ], | ||||
|       "dependencies": { | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "core-util-is": "1.0.2", | ||||
|         "extsprintf": "^1.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/yauzl": { | ||||
|       "version": "2.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", | ||||
|       "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", | ||||
|       "dependencies": { | ||||
|         "buffer-crc32": "~0.2.3", | ||||
|         "fd-slicer": "~1.1.0" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "ajv": { | ||||
|       "version": "6.12.6", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", | ||||
|       "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", | ||||
|       "requires": { | ||||
|         "fast-deep-equal": "^3.1.1", | ||||
|         "fast-json-stable-stringify": "^2.0.0", | ||||
|         "json-schema-traverse": "^0.4.1", | ||||
|         "uri-js": "^4.2.2" | ||||
|       } | ||||
|     }, | ||||
|     "asn1": { | ||||
|       "version": "0.2.6", | ||||
|       "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", | ||||
|       "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", | ||||
|       "requires": { | ||||
|         "safer-buffer": "~2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "assert-plus": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", | ||||
|       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" | ||||
|     }, | ||||
|     "asynckit": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", | ||||
|       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" | ||||
|     }, | ||||
|     "aws-sign2": { | ||||
|       "version": "0.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", | ||||
|       "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" | ||||
|     }, | ||||
|     "aws4": { | ||||
|       "version": "1.11.0", | ||||
|       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", | ||||
|       "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" | ||||
|     }, | ||||
|     "bcrypt-pbkdf": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", | ||||
|       "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", | ||||
|       "requires": { | ||||
|         "tweetnacl": "^0.14.3" | ||||
|       } | ||||
|     }, | ||||
|     "buffer-crc32": { | ||||
|       "version": "0.2.13", | ||||
|       "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", | ||||
|       "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" | ||||
|     }, | ||||
|     "caseless": { | ||||
|       "version": "0.12.0", | ||||
|       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", | ||||
|       "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" | ||||
|     }, | ||||
|     "combined-stream": { | ||||
|       "version": "1.0.8", | ||||
|       "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", | ||||
|       "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", | ||||
|       "requires": { | ||||
|         "delayed-stream": "~1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "core-util-is": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", | ||||
|       "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" | ||||
|     }, | ||||
|     "dashdash": { | ||||
|       "version": "1.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", | ||||
|       "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", | ||||
|       "requires": { | ||||
|         "assert-plus": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "delayed-stream": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", | ||||
|       "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" | ||||
|     }, | ||||
|     "ecc-jsbn": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", | ||||
|       "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", | ||||
|       "requires": { | ||||
|         "jsbn": "~0.1.0", | ||||
|         "safer-buffer": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "extend": { | ||||
|       "version": "3.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", | ||||
|       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" | ||||
|     }, | ||||
|     "extsprintf": { | ||||
|       "version": "1.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", | ||||
|       "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" | ||||
|     }, | ||||
|     "fast-deep-equal": { | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | ||||
|       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" | ||||
|     }, | ||||
|     "fast-json-stable-stringify": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", | ||||
|       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" | ||||
|     }, | ||||
|     "fd-slicer": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", | ||||
|       "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", | ||||
|       "requires": { | ||||
|         "pend": "~1.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "forever-agent": { | ||||
|       "version": "0.6.1", | ||||
|       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", | ||||
|       "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" | ||||
|     }, | ||||
|     "form-data": { | ||||
|       "version": "2.3.3", | ||||
|       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", | ||||
|       "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", | ||||
|       "requires": { | ||||
|         "asynckit": "^0.4.0", | ||||
|         "combined-stream": "^1.0.6", | ||||
|         "mime-types": "^2.1.12" | ||||
|       } | ||||
|     }, | ||||
|     "getpass": { | ||||
|       "version": "0.1.7", | ||||
|       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", | ||||
|       "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", | ||||
|       "requires": { | ||||
|         "assert-plus": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "har-schema": { | ||||
|       "version": "2.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", | ||||
|       "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" | ||||
|     }, | ||||
|     "har-validator": { | ||||
|       "version": "5.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", | ||||
|       "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", | ||||
|       "requires": { | ||||
|         "ajv": "^6.12.3", | ||||
|         "har-schema": "^2.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "http-signature": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", | ||||
|       "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", | ||||
|       "requires": { | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "jsprim": "^1.2.2", | ||||
|         "sshpk": "^1.7.0" | ||||
|       } | ||||
|     }, | ||||
|     "is-typedarray": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", | ||||
|       "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" | ||||
|     }, | ||||
|     "isstream": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", | ||||
|       "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" | ||||
|     }, | ||||
|     "jsbn": { | ||||
|       "version": "0.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", | ||||
|       "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" | ||||
|     }, | ||||
|     "json-schema": { | ||||
|       "version": "0.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", | ||||
|       "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" | ||||
|     }, | ||||
|     "json-schema-traverse": { | ||||
|       "version": "0.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", | ||||
|       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" | ||||
|     }, | ||||
|     "json-stringify-safe": { | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", | ||||
|       "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" | ||||
|     }, | ||||
|     "jsprim": { | ||||
|       "version": "1.4.2", | ||||
|       "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", | ||||
|       "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", | ||||
|       "requires": { | ||||
|         "assert-plus": "1.0.0", | ||||
|         "extsprintf": "1.3.0", | ||||
|         "json-schema": "0.4.0", | ||||
|         "verror": "1.10.0" | ||||
|       } | ||||
|     }, | ||||
|     "mime-db": { | ||||
|       "version": "1.51.0", | ||||
|       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", | ||||
|       "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" | ||||
|     }, | ||||
|     "mime-types": { | ||||
|       "version": "2.1.34", | ||||
|       "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", | ||||
|       "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", | ||||
|       "requires": { | ||||
|         "mime-db": "1.51.0" | ||||
|       } | ||||
|     }, | ||||
|     "minimist": { | ||||
|       "version": "1.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", | ||||
|       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" | ||||
|     }, | ||||
|     "mkdirp": { | ||||
|       "version": "0.5.5", | ||||
|       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", | ||||
|       "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", | ||||
|       "requires": { | ||||
|         "minimist": "^1.2.5" | ||||
|       } | ||||
|     }, | ||||
|     "oauth-sign": { | ||||
|       "version": "0.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", | ||||
|       "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" | ||||
|     }, | ||||
|     "pend": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", | ||||
|       "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" | ||||
|     }, | ||||
|     "performance-now": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", | ||||
|       "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" | ||||
|     }, | ||||
|     "psl": { | ||||
|       "version": "1.8.0", | ||||
|       "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", | ||||
|       "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" | ||||
|     }, | ||||
|     "punycode": { | ||||
|       "version": "1.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", | ||||
|       "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" | ||||
|     }, | ||||
|     "qs": { | ||||
|       "version": "6.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", | ||||
|       "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" | ||||
|     }, | ||||
|     "request": { | ||||
|       "version": "2.88.0", | ||||
|       "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", | ||||
|       "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", | ||||
|       "requires": { | ||||
|         "aws-sign2": "~0.7.0", | ||||
|         "aws4": "^1.8.0", | ||||
|         "caseless": "~0.12.0", | ||||
|         "combined-stream": "~1.0.6", | ||||
|         "extend": "~3.0.2", | ||||
|         "forever-agent": "~0.6.1", | ||||
|         "form-data": "~2.3.2", | ||||
|         "har-validator": "~5.1.0", | ||||
|         "http-signature": "~1.2.0", | ||||
|         "is-typedarray": "~1.0.0", | ||||
|         "isstream": "~0.1.2", | ||||
|         "json-stringify-safe": "~5.0.1", | ||||
|         "mime-types": "~2.1.19", | ||||
|         "oauth-sign": "~0.9.0", | ||||
|         "performance-now": "^2.1.0", | ||||
|         "qs": "~6.5.2", | ||||
|         "safe-buffer": "^5.1.2", | ||||
|         "tough-cookie": "~2.4.3", | ||||
|         "tunnel-agent": "^0.6.0", | ||||
|         "uuid": "^3.3.2" | ||||
|       } | ||||
|     }, | ||||
|     "safe-buffer": { | ||||
|       "version": "5.2.1", | ||||
|       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", | ||||
|       "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" | ||||
|     }, | ||||
|     "safer-buffer": { | ||||
|       "version": "2.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", | ||||
|       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" | ||||
|     }, | ||||
|     "sshpk": { | ||||
|       "version": "1.16.1", | ||||
|       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", | ||||
|       "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", | ||||
|       "requires": { | ||||
|         "asn1": "~0.2.3", | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "bcrypt-pbkdf": "^1.0.0", | ||||
|         "dashdash": "^1.12.0", | ||||
|         "ecc-jsbn": "~0.1.1", | ||||
|         "getpass": "^0.1.1", | ||||
|         "jsbn": "~0.1.0", | ||||
|         "safer-buffer": "^2.0.2", | ||||
|         "tweetnacl": "~0.14.0" | ||||
|       } | ||||
|     }, | ||||
|     "tough-cookie": { | ||||
|       "version": "2.4.3", | ||||
|       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", | ||||
|       "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", | ||||
|       "requires": { | ||||
|         "psl": "^1.1.24", | ||||
|         "punycode": "^1.4.1" | ||||
|       } | ||||
|     }, | ||||
|     "tunnel-agent": { | ||||
|       "version": "0.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", | ||||
|       "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", | ||||
|       "requires": { | ||||
|         "safe-buffer": "^5.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "tweetnacl": { | ||||
|       "version": "0.14.5", | ||||
|       "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", | ||||
|       "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" | ||||
|     }, | ||||
|     "uri-js": { | ||||
|       "version": "4.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", | ||||
|       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", | ||||
|       "requires": { | ||||
|         "punycode": "^2.1.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "punycode": { | ||||
|           "version": "2.1.1", | ||||
|           "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", | ||||
|           "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "uuid": { | ||||
|       "version": "3.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", | ||||
|       "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" | ||||
|     }, | ||||
|     "verror": { | ||||
|       "version": "1.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", | ||||
|       "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", | ||||
|       "requires": { | ||||
|         "assert-plus": "^1.0.0", | ||||
|         "core-util-is": "1.0.2", | ||||
|         "extsprintf": "^1.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "yauzl": { | ||||
|       "version": "2.10.0", | ||||
|       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", | ||||
|       "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", | ||||
|       "requires": { | ||||
|         "buffer-crc32": "~0.2.3", | ||||
|         "fd-slicer": "~1.1.0" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | @ -1,9 +1,12 @@ | |||
| { | ||||
|   "name": "openasar", | ||||
|   "description": "Discord Client for Desktop - Bootstrapper", | ||||
|   "description": "Open-source alternative of Discord desktop's app.asar", | ||||
| 
 | ||||
|   "main": "index.js", | ||||
| 
 | ||||
|   "dependencies": { | ||||
|     "mkdirp": "^0.5.1", | ||||
|     "request": "2.88.0", | ||||
|     "yauzl": "^2.10.0" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/paths.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/paths.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| const { join, dirname, basename } = require('path'); | ||||
| const { app } = require('electron'); | ||||
| 
 | ||||
| const log = require('./utils/log'); | ||||
| const buildInfo = require('./utils/buildInfo'); | ||||
| 
 | ||||
| 
 | ||||
| const appDir = 'discord' + (buildInfo.releaseChannel === 'stable' ? '' : buildInfo.releaseChannel); // Clean channel naming up later to util?
 | ||||
| const userData = join(app.getPath('appData'), appDir); | ||||
| const userDataVersioned = join(userData, buildInfo.version); | ||||
| 
 | ||||
| const exeDir = dirname(app.getPath('exe')); | ||||
| const installPath = /^app-[0-9]+\.[0-9]+\.[0-9]+/.test(basename(exeDir)) ? join(exeDir, '..') : null; | ||||
| 
 | ||||
| const moduleData = buildInfo.newUpdater ? join(userData, 'module_data') : join(userDataVersioned, 'modules'); | ||||
| 
 | ||||
| 
 | ||||
| exports.getUserData = () => userData; | ||||
| exports.getUserDataVersioned = () => userDataVersioned; | ||||
| 
 | ||||
| exports.getResources = () => process.resourcesPath; // Discord uses path and require.main.filename here because ??
 | ||||
| exports.getModuleDataPath = () => moduleData; | ||||
| exports.getInstallPath = () => installPath; | ||||
| 
 | ||||
| exports.init = () => {}; // Stub as we setup on require
 | ||||
							
								
								
									
										89
									
								
								src/splash/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/splash/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,89 @@ | |||
| <div id="text">openasar go brr</div> | ||||
| 
 | ||||
| <div id="bar-container"></div> | ||||
| <div id="bar-fill"></div> | ||||
| 
 | ||||
| <style> | ||||
|   html, body { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
| 
 | ||||
|     background: #101418; | ||||
|   } | ||||
| 
 | ||||
|   * { | ||||
|     color: rgba(255, 255, 255, 0.97); | ||||
|     font-family: sans-serif; | ||||
|   } | ||||
| 
 | ||||
|   #text { | ||||
|     position: absolute; | ||||
|     top: 45%; | ||||
|     left: 50%; | ||||
|     transform: translate(-50%, -50%); | ||||
| 
 | ||||
|     font-size: 7vw; | ||||
|     text-align: center; | ||||
| 
 | ||||
|     text-transform: capitalize; | ||||
| 
 | ||||
|     width: 100%; | ||||
|   } | ||||
| 
 | ||||
|   #bar-container, #bar-fill { | ||||
|     position: absolute; | ||||
|     top: 58%; | ||||
|     left: 50%; | ||||
|     transform: translate(-50%, -50%); | ||||
| 
 | ||||
|     width: 80%; | ||||
|     height: 5%; | ||||
| 
 | ||||
|     border-radius: 4px; | ||||
| 
 | ||||
|     display: none; | ||||
|   } | ||||
| 
 | ||||
|   #bar-container { | ||||
|     background: #202428; | ||||
|   } | ||||
| 
 | ||||
|   #bar-fill { | ||||
|     background: rgb(88, 101, 242); | ||||
|     width: 0; | ||||
| 
 | ||||
|     transform: translate(0%, -50%); | ||||
|     left: 10%; | ||||
|   } | ||||
| </style> | ||||
| 
 | ||||
| 
 | ||||
| <script> | ||||
|   const text = document.querySelector('#text'); | ||||
| 
 | ||||
|   const barContainer = document.querySelector('#bar-container'); | ||||
|   const barFill = document.querySelector('#bar-fill'); | ||||
| 
 | ||||
|   DiscordSplash.signalReady(); | ||||
| 
 | ||||
| 
 | ||||
|   DiscordSplash.onStateUpdate(({ status, current, total, progress }) => { | ||||
|     console.log('onStateUpdate', progress); | ||||
| 
 | ||||
|     text.textContent = status.replaceAll('-', ' '); | ||||
| 
 | ||||
|     if (progress) { | ||||
|       barContainer.style.display = 'block'; | ||||
|       barFill.style.display = 'block'; | ||||
| 
 | ||||
|       barFill.style.width = 80 * (progress / 100) + '%'; | ||||
|     } else { | ||||
|       barContainer.style.display = ''; | ||||
|       barFill.style.display = ''; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   DiscordSplash.onQuoteUpdate((e) => { | ||||
|     console.log('onQuoteUpdate', e); | ||||
|   }) | ||||
| </script> | ||||
							
								
								
									
										36
									
								
								src/splash/preload.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/splash/preload.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| const { | ||||
|   app, | ||||
|   contextBridge, | ||||
|   ipcRenderer | ||||
| } = require('electron'); | ||||
| 
 | ||||
| const { | ||||
|   saferShellOpenExternal | ||||
| } = require('../utils/securityUtils'); | ||||
| 
 | ||||
| contextBridge.exposeInMainWorld('DiscordSplash', { | ||||
|   getReleaseChannel: () => { | ||||
|     const buildInfo = require('../utils/buildInfo'); | ||||
| 
 | ||||
|     return buildInfo.releaseChannel; | ||||
|   }, | ||||
|   signalReady: () => { | ||||
|     ipcRenderer.send('DISCORD_SPLASH_SCREEN_READY'); | ||||
|   }, | ||||
|   onStateUpdate: callback => { | ||||
|     ipcRenderer.on('DISCORD_SPLASH_UPDATE_STATE', (_, state) => { | ||||
|       callback(state); | ||||
|     }); | ||||
|   }, | ||||
|   onQuoteUpdate: callback => { | ||||
|     ipcRenderer.on('DISCORD_SPLASH_SCREEN_QUOTE', (_, quote) => { | ||||
|       callback(quote); | ||||
|     }); | ||||
|   }, | ||||
|   openUrl: saferShellOpenExternal, | ||||
|   quitDiscord: () => { | ||||
|     ipcRenderer.send('DISCORD_SPLASH_SCREEN_QUIT'); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										486
									
								
								src/splash/splashScreen.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										486
									
								
								src/splash/splashScreen.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,486 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.initSplash = initSplash; | ||||
| exports.focusWindow = focusWindow; | ||||
| exports.pageReady = pageReady; | ||||
| exports.events = exports.APP_SHOULD_SHOW = exports.APP_SHOULD_LAUNCH = void 0; | ||||
| 
 | ||||
| var _electron = require("electron"); | ||||
| 
 | ||||
| var _events = require("events"); | ||||
| 
 | ||||
| var _fs = _interopRequireDefault(require("fs")); | ||||
| 
 | ||||
| var _path = _interopRequireDefault(require("path")); | ||||
| 
 | ||||
| var _url = _interopRequireDefault(require("url")); | ||||
| 
 | ||||
| var _Backoff = _interopRequireDefault(require("../utils/Backoff")); | ||||
| 
 | ||||
| var moduleUpdater = _interopRequireWildcard(require("../updater/moduleUpdater")); | ||||
| 
 | ||||
| var paths = _interopRequireWildcard(require("../paths")); | ||||
| 
 | ||||
| var _securityUtils = require("../utils/securityUtils"); | ||||
| 
 | ||||
| var _updater = require("../updater/updater"); | ||||
| 
 | ||||
| var _ipcMain = _interopRequireDefault(require("../ipcMain")); | ||||
| 
 | ||||
| function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||||
| 
 | ||||
| function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| const UPDATE_TIMEOUT_WAIT = 10000; | ||||
| const RETRY_CAP_SECONDS = 60; // citron note: atom seems to add about 50px height to the frame on mac but not windows
 | ||||
| // TODO: see if we can eliminate fudge by using useContentSize BrowserWindow option
 | ||||
| 
 | ||||
| const LOADING_WINDOW_WIDTH = 300; | ||||
| const LOADING_WINDOW_HEIGHT = process.platform === 'darwin' ? 300 : 350; // TODO: addModulesListener events should use Module's constants
 | ||||
| 
 | ||||
| const CHECKING_FOR_UPDATES = 'checking-for-updates'; | ||||
| const UPDATE_CHECK_FINISHED = 'update-check-finished'; | ||||
| const UPDATE_FAILURE = 'update-failure'; | ||||
| const LAUNCHING = 'launching'; | ||||
| const DOWNLOADING_MODULE = 'downloading-module'; | ||||
| const DOWNLOADING_UPDATES = 'downloading-updates'; | ||||
| const DOWNLOADING_MODULES_FINISHED = 'downloading-modules-finished'; | ||||
| const DOWNLOADING_MODULE_PROGRESS = 'downloading-module-progress'; | ||||
| const DOWNLOADED_MODULE = 'downloaded-module'; | ||||
| const NO_PENDING_UPDATES = 'no-pending-updates'; | ||||
| const INSTALLING_MODULE = 'installing-module'; | ||||
| const INSTALLING_UPDATES = 'installing-updates'; | ||||
| const INSTALLED_MODULE = 'installed-module'; | ||||
| const INSTALLING_MODULE_PROGRESS = 'installing-module-progress'; | ||||
| const INSTALLING_MODULES_FINISHED = 'installing-modules-finished'; | ||||
| const UPDATE_MANUALLY = 'update-manually'; | ||||
| const APP_SHOULD_LAUNCH = 'APP_SHOULD_LAUNCH'; | ||||
| exports.APP_SHOULD_LAUNCH = APP_SHOULD_LAUNCH; | ||||
| const APP_SHOULD_SHOW = 'APP_SHOULD_SHOW'; | ||||
| exports.APP_SHOULD_SHOW = APP_SHOULD_SHOW; | ||||
| const events = new _events.EventEmitter(); | ||||
| exports.events = events; | ||||
| 
 | ||||
| function webContentsSend(win, event, ...args) { | ||||
|   if (win != null && win.webContents != null) { | ||||
|     win.webContents.send(`DISCORD_${event}`, ...args); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| let splashWindow; | ||||
| let modulesListeners; | ||||
| let updateTimeout; | ||||
| let updateAttempt; | ||||
| let splashState; | ||||
| let launchedMainWindow; | ||||
| let quoteCachePath; | ||||
| let restartRequired = false; | ||||
| let newUpdater; | ||||
| const updateBackoff = new _Backoff.default(1000, 30000); // TODO(eiz): some of this logic should probably not live in the splash.
 | ||||
| //
 | ||||
| // Disabled because Rust interop stuff is going on in here.
 | ||||
| 
 | ||||
| /* eslint-disable camelcase */ | ||||
| 
 | ||||
| class TaskProgress { | ||||
|   constructor() { | ||||
|     this.inProgress = new Map(); | ||||
|     this.finished = new Set(); | ||||
|     this.allTasks = new Set(); | ||||
|   } | ||||
| 
 | ||||
|   recordProgress(progress, task) { | ||||
|     this.allTasks.add(task.package_sha256); | ||||
| 
 | ||||
|     if (progress.state !== _updater.TASK_STATE_WAITING) { | ||||
|       this.inProgress.set(task.package_sha256, progress.percent); | ||||
| 
 | ||||
|       if (progress.state === _updater.TASK_STATE_COMPLETE) { | ||||
|         this.finished.add(task.package_sha256); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   updateSplashState(newState) { | ||||
|     if (this.inProgress.size > 0 && this.inProgress.size > this.finished.size) { | ||||
|       let totalPercent = 0; | ||||
| 
 | ||||
|       for (const item of this.inProgress.values()) { | ||||
|         totalPercent += item; | ||||
|       } | ||||
| 
 | ||||
|       totalPercent /= this.allTasks.size; | ||||
|       splashState = { | ||||
|         current: this.finished.size + 1, | ||||
|         total: this.allTasks.size, | ||||
|         progress: totalPercent | ||||
|       }; | ||||
|       updateSplashState(newState); | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| async function updateUntilCurrent() { | ||||
|   const retryOptions = { | ||||
|     skip_host_delta: false, | ||||
|     skip_module_delta: {} | ||||
|   }; | ||||
| 
 | ||||
|   while (true) { | ||||
|     updateSplashState(CHECKING_FOR_UPDATES); | ||||
| 
 | ||||
|     try { | ||||
|       let installedAnything = false; | ||||
|       const downloads = new TaskProgress(); | ||||
|       const installs = new TaskProgress(); | ||||
|       await newUpdater.updateToLatestWithOptions(retryOptions, progress => { | ||||
|         const task = progress.task; | ||||
|         const downloadTask = task.HostDownload || task.ModuleDownload; | ||||
|         const installTask = task.HostInstall || task.ModuleInstall; | ||||
|         installedAnything = true; | ||||
| 
 | ||||
|         if (downloadTask != null) { | ||||
|           downloads.recordProgress(progress, downloadTask); | ||||
|         } | ||||
| 
 | ||||
|         if (installTask != null) { | ||||
|           installs.recordProgress(progress, installTask); | ||||
| 
 | ||||
|           if (progress.state.Failed != null) { | ||||
|             if (task.HostInstall != null) { | ||||
|               retryOptions.skip_host_delta = true; | ||||
|             } else if (task.ModuleInstall != null) { | ||||
|               retryOptions.skip_module_delta[installTask.version.module.name] = true; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (!downloads.updateSplashState(DOWNLOADING_UPDATES)) { | ||||
|           installs.updateSplashState(INSTALLING_UPDATES); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       if (!installedAnything) { | ||||
|         await newUpdater.startCurrentVersion(); | ||||
|         newUpdater.setRunningInBackground(); | ||||
|         newUpdater.collectGarbage(); | ||||
|         launchMainWindow(); | ||||
|         updateBackoff.succeed(); | ||||
|         updateSplashState(LAUNCHING); | ||||
|         return; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.error('Update failed', e); | ||||
|       await new Promise(resolve => { | ||||
|         const delayMs = updateBackoff.fail(resolve); | ||||
|         splashState.seconds = Math.round(delayMs / 1000); | ||||
|         updateSplashState(UPDATE_FAILURE); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| /* eslint-enable camelcase */ | ||||
| 
 | ||||
| 
 | ||||
| function initOldUpdater() { | ||||
|   modulesListeners = {}; | ||||
|   addModulesListener(CHECKING_FOR_UPDATES, () => { | ||||
|     startUpdateTimeout(); | ||||
|     updateSplashState(CHECKING_FOR_UPDATES); | ||||
|   }); | ||||
|   addModulesListener(UPDATE_CHECK_FINISHED, ({ | ||||
|     succeeded, | ||||
|     updateCount, | ||||
|     manualRequired | ||||
|   }) => { | ||||
|     stopUpdateTimeout(); | ||||
| 
 | ||||
|     if (!succeeded) { | ||||
|       scheduleUpdateCheck(); | ||||
|       updateSplashState(UPDATE_FAILURE); | ||||
|     } else if (updateCount === 0) { | ||||
|       moduleUpdater.setInBackground(); | ||||
|       launchMainWindow(); | ||||
|       updateSplashState(LAUNCHING); | ||||
|     } | ||||
|   }); | ||||
|   addModulesListener(DOWNLOADING_MODULE, ({ | ||||
|     name, | ||||
|     current, | ||||
|     total | ||||
|   }) => { | ||||
|     stopUpdateTimeout(); | ||||
|     splashState = { | ||||
|       current, | ||||
|       total | ||||
|     }; | ||||
|     updateSplashState(DOWNLOADING_UPDATES); | ||||
|   }); | ||||
|   addModulesListener(DOWNLOADING_MODULE_PROGRESS, ({ | ||||
|     name, | ||||
|     progress | ||||
|   }) => { | ||||
|     splashState.progress = progress; | ||||
|     updateSplashState(DOWNLOADING_UPDATES); | ||||
|   }); | ||||
|   addModulesListener(DOWNLOADED_MODULE, ({ | ||||
|     name, | ||||
|     current, | ||||
|     total, | ||||
|     succeeded | ||||
|   }) => { | ||||
|     delete splashState.progress; | ||||
| 
 | ||||
|     if (name === 'host') { | ||||
|       restartRequired = true; | ||||
|     } | ||||
|   }); | ||||
|   addModulesListener(DOWNLOADING_MODULES_FINISHED, ({ | ||||
|     succeeded, | ||||
|     failed | ||||
|   }) => { | ||||
|     if (failed > 0) { | ||||
|       scheduleUpdateCheck(); | ||||
|       updateSplashState(UPDATE_FAILURE); | ||||
|     } else { | ||||
|       process.nextTick(() => { | ||||
|         if (restartRequired) { | ||||
|           moduleUpdater.quitAndInstallUpdates(); | ||||
|         } else { | ||||
|           moduleUpdater.installPendingUpdates(); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|   addModulesListener(NO_PENDING_UPDATES, () => moduleUpdater.checkForUpdates()); | ||||
|   addModulesListener(INSTALLING_MODULE, ({ | ||||
|     name, | ||||
|     current, | ||||
|     total | ||||
|   }) => { | ||||
|     splashState = { | ||||
|       current, | ||||
|       total | ||||
|     }; | ||||
|     updateSplashState(INSTALLING_UPDATES); | ||||
|   }); | ||||
|   addModulesListener(INSTALLED_MODULE, ({ | ||||
|     name, | ||||
|     current, | ||||
|     total, | ||||
|     succeeded | ||||
|   }) => delete splashState.progress); | ||||
|   addModulesListener(INSTALLING_MODULE_PROGRESS, ({ | ||||
|     name, | ||||
|     progress | ||||
|   }) => { | ||||
|     splashState.progress = progress; | ||||
|     updateSplashState(INSTALLING_UPDATES); | ||||
|   }); | ||||
|   addModulesListener(INSTALLING_MODULES_FINISHED, ({ | ||||
|     succeeded, | ||||
|     failed | ||||
|   }) => moduleUpdater.checkForUpdates()); | ||||
|   addModulesListener(UPDATE_MANUALLY, ({ | ||||
|     newVersion | ||||
|   }) => { | ||||
|     splashState.newVersion = newVersion; | ||||
|     updateSplashState(UPDATE_MANUALLY); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function initSplash(startMinimized = false) { | ||||
|   splashState = {}; | ||||
|   launchedMainWindow = false; | ||||
|   updateAttempt = 0; | ||||
|   newUpdater = (0, _updater.getUpdater)(); | ||||
| 
 | ||||
|   if (newUpdater == null) { | ||||
|     initOldUpdater(); | ||||
|   } | ||||
| 
 | ||||
|   launchSplashWindow(startMinimized); | ||||
|   quoteCachePath = _path.default.join(paths.getUserData(), 'quotes.json'); | ||||
| 
 | ||||
|   _ipcMain.default.on('UPDATED_QUOTES', (_event, quotes) => cacheLatestQuotes(quotes)); | ||||
| } | ||||
| 
 | ||||
| function destroySplash() { | ||||
|   stopUpdateTimeout(); | ||||
| 
 | ||||
|   if (splashWindow) { | ||||
|     splashWindow.setSkipTaskbar(true); // defer the window hiding for a short moment so it gets covered by the main window
 | ||||
| 
 | ||||
|     const _nukeWindow = () => { | ||||
|       if (splashWindow != null) { | ||||
|         splashWindow.hide(); | ||||
|         splashWindow.close(); | ||||
|         splashWindow = null; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     setTimeout(_nukeWindow, 100); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function addModulesListener(event, listener) { | ||||
|   if (newUpdater != null) return; | ||||
|   modulesListeners[event] = listener; | ||||
|   moduleUpdater.events.addListener(event, listener); | ||||
| } | ||||
| 
 | ||||
| function removeModulesListeners() { | ||||
|   if (newUpdater != null) return; | ||||
| 
 | ||||
|   for (const event of Object.keys(modulesListeners)) { | ||||
|     moduleUpdater.events.removeListener(event, modulesListeners[event]); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function startUpdateTimeout() { | ||||
|   if (!updateTimeout) { | ||||
|     updateTimeout = setTimeout(() => scheduleUpdateCheck(), UPDATE_TIMEOUT_WAIT); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function stopUpdateTimeout() { | ||||
|   if (updateTimeout) { | ||||
|     clearTimeout(updateTimeout); | ||||
|     updateTimeout = null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function updateSplashState(event) { | ||||
|   if (splashWindow != null && !splashWindow.isDestroyed() && !splashWindow.webContents.isDestroyed()) { | ||||
|     webContentsSend(splashWindow, 'SPLASH_UPDATE_STATE', { | ||||
|       status: event, | ||||
|       ...splashState | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function launchSplashWindow(startMinimized) { | ||||
|   const windowConfig = { | ||||
|     width: LOADING_WINDOW_WIDTH, | ||||
|     height: LOADING_WINDOW_HEIGHT, | ||||
|     transparent: false, | ||||
|     frame: false, | ||||
|     resizable: false, | ||||
|     center: true, | ||||
|     show: false, | ||||
|     webPreferences: { | ||||
|       nodeIntegration: false, | ||||
|       enableRemoteModule: false, | ||||
|       contextIsolation: true, | ||||
|       preload: _path.default.join(__dirname, 'preload.js') | ||||
|     } | ||||
|   }; | ||||
|   splashWindow = new _electron.BrowserWindow(windowConfig); // prevent users from dropping links to navigate in splash window
 | ||||
| 
 | ||||
|   splashWindow.webContents.on('will-navigate', e => e.preventDefault()); | ||||
|   splashWindow.webContents.on('new-window', (e, windowURL) => { | ||||
|     e.preventDefault(); | ||||
|     (0, _securityUtils.saferShellOpenExternal)(windowURL); // exit, but delay half a second because openExternal is about to fire
 | ||||
|     // some events to things that are freed by app.quit.
 | ||||
| 
 | ||||
|     setTimeout(_electron.app.quit, 500); | ||||
|   }); | ||||
| 
 | ||||
|   if (process.platform !== 'darwin') { | ||||
|     // citron note: this causes a crash on quit while the window is open on osx
 | ||||
|     splashWindow.on('closed', () => { | ||||
|       splashWindow = null; | ||||
| 
 | ||||
|       if (!launchedMainWindow) { | ||||
|         // user has closed this window before we launched the app, so let's quit
 | ||||
|         _electron.app.quit(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   _ipcMain.default.on('SPLASH_SCREEN_READY', () => { | ||||
|     const cachedQuote = chooseCachedQuote(); | ||||
| 
 | ||||
|     if (cachedQuote) { | ||||
|       webContentsSend(splashWindow, 'SPLASH_SCREEN_QUOTE', cachedQuote); | ||||
|     } | ||||
| 
 | ||||
|     if (splashWindow && !startMinimized) { | ||||
|       splashWindow.show(); | ||||
|     } | ||||
| 
 | ||||
|     if (newUpdater != null) { | ||||
|       updateUntilCurrent(); | ||||
|     } else { | ||||
|       moduleUpdater.installPendingUpdates(); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   _ipcMain.default.on('SPLASH_SCREEN_QUIT', () => { | ||||
|     _electron.app.quit(); | ||||
|   }); | ||||
| 
 | ||||
|   const splashUrl = _url.default.format({ | ||||
|     protocol: 'file', | ||||
|     slashes: true, | ||||
|     pathname: _path.default.join(__dirname, 'index.html') | ||||
|   }); | ||||
| 
 | ||||
|   splashWindow.loadURL(splashUrl); | ||||
| } | ||||
| 
 | ||||
| function launchMainWindow() { | ||||
|   removeModulesListeners(); | ||||
| 
 | ||||
|   if (!launchedMainWindow && splashWindow != null) { | ||||
|     launchedMainWindow = true; | ||||
|     events.emit(APP_SHOULD_LAUNCH); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function scheduleUpdateCheck() { | ||||
|   // TODO: can we use backoff here?
 | ||||
|   updateAttempt += 1; | ||||
|   const retryInSeconds = Math.min(updateAttempt * 10, RETRY_CAP_SECONDS); | ||||
|   splashState.seconds = retryInSeconds; | ||||
|   setTimeout(() => moduleUpdater.checkForUpdates(), retryInSeconds * 1000); | ||||
| } | ||||
| 
 | ||||
| function focusWindow() { | ||||
|   if (splashWindow != null) { | ||||
|     splashWindow.focus(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function pageReady() { | ||||
|   destroySplash(); | ||||
|   process.nextTick(() => events.emit(APP_SHOULD_SHOW)); | ||||
| } | ||||
| 
 | ||||
| function cacheLatestQuotes(quotes) { | ||||
|   _fs.default.writeFile(quoteCachePath, JSON.stringify(quotes), e => { | ||||
|     if (e) { | ||||
|       console.warn('Failed updating quote cache with error: ', e); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function chooseCachedQuote() { | ||||
|   let cachedQuote = null; | ||||
| 
 | ||||
|   try { | ||||
|     const cachedQuotes = JSON.parse(_fs.default.readFileSync(quoteCachePath)); | ||||
|     cachedQuote = cachedQuotes[Math.floor(Math.random() * cachedQuotes.length)]; | ||||
|   } catch (_err) {} | ||||
| 
 | ||||
|   return cachedQuote; | ||||
| } | ||||
							
								
								
									
										95
									
								
								src/splash/splashScreen.js.self
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/splash/splashScreen.js.self
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | |||
| const { BrowserWindow, app } = require('electron'); | ||||
| 
 | ||||
| const { format } = require('url'); | ||||
| const { join } = require('path'); | ||||
| 
 | ||||
| const ipcMain = require('../ipcMain'); | ||||
| 
 | ||||
| 
 | ||||
| const LOADING_WINDOW_WIDTH = 300; | ||||
| const LOADING_WINDOW_HEIGHT = process.platform === 'darwin' ? 300 : 350; // TODO: addModulesListener events should use Module's constants | ||||
| 
 | ||||
| let window; | ||||
| 
 | ||||
| const APP_SHOULD_LAUNCH = 'APP_SHOULD_LAUNCH'; | ||||
| exports.APP_SHOULD_LAUNCH = APP_SHOULD_LAUNCH; | ||||
| const APP_SHOULD_SHOW = 'APP_SHOULD_SHOW'; | ||||
| exports.APP_SHOULD_SHOW = APP_SHOULD_SHOW; | ||||
| const events = new (require('events')).EventEmitter(); | ||||
| 
 | ||||
| exports.events = events; | ||||
| 
 | ||||
| exports.initSplash = (startMinimized = false) => { // Make splash window | ||||
|   const windowConfig = { | ||||
|     width: LOADING_WINDOW_WIDTH, | ||||
|     height: LOADING_WINDOW_HEIGHT, | ||||
|     transparent: false, | ||||
|     frame: false, | ||||
|     resizable: false, | ||||
|     center: true, | ||||
|     show: false, | ||||
|     webPreferences: { | ||||
|       nodeIntegration: false, | ||||
|       enableRemoteModule: false, | ||||
|       contextIsolation: true, | ||||
|       preload: join(__dirname, 'preload.js') | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   window = new BrowserWindow(windowConfig); | ||||
| 
 | ||||
|   window.on('closed', () => { // Quit app on splash screen close | ||||
|     app.quit(); | ||||
|     window = null; | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   // IPC "handlers" | ||||
|   ipcMain.on('SPLASH_SCREEN_READY', () => { | ||||
|     if (!startMinimized && window) window.show(); | ||||
| 
 | ||||
|     // Update and stuff | ||||
| 
 | ||||
|     events.emit(APP_SHOULD_LAUNCH); | ||||
|   }); | ||||
| 
 | ||||
|   if (!startMinimized && window) window.show(); | ||||
| 
 | ||||
|   // Update and stuff | ||||
| 
 | ||||
|   events.emit(APP_SHOULD_LAUNCH); | ||||
| 
 | ||||
|   ipcMain.on('SPLASH_SCREEN_QUIT', () => { | ||||
|     app.quit(); | ||||
|   }); | ||||
| 
 | ||||
|   const splashUrl = format({ | ||||
|     protocol: 'file', | ||||
|     slashes: true, | ||||
|     pathname: join(__dirname, 'index.html') | ||||
|   }); | ||||
| 
 | ||||
|   window.loadURL(splashUrl); | ||||
| }; | ||||
| 
 | ||||
| exports.focusWindow = () => { // Focus splash window | ||||
|   if (window) window.focus(); | ||||
| }; | ||||
| 
 | ||||
| const killWindow = () => { | ||||
|   if (!window) return; | ||||
| 
 | ||||
|   window.setSkipTaskbar(true); | ||||
| 
 | ||||
|   setTimeout(() => { | ||||
|     window.hide(); | ||||
|     window.close(); | ||||
|     window = null; | ||||
|   }, 100); | ||||
| }; | ||||
| 
 | ||||
| exports.pageReady = () => { // Kill splash window, emit | ||||
|   killWindow(); | ||||
| 
 | ||||
|   process.nextTick(() => events.emit(APP_SHOULD_SHOW)); | ||||
| }; | ||||
							
								
								
									
										
											BIN
										
									
								
								src/splash/web/a934ab008c7f6a2274ec441f6be0696a.woff
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/splash/web/a934ab008c7f6a2274ec441f6be0696a.woff
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/splash/web/abddffb32a4a35627c3857a06c751424.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/splash/web/abddffb32a4a35627c3857a06c751424.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/splash/web/d153359b5d87601d2b9c708b7ae2db02.woff
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/splash/web/d153359b5d87601d2b9c708b7ae2db02.woff
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										11
									
								
								src/splash/web/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/splash/web/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|   <meta charset="utf-8" /> | ||||
|   <title>Discord Updater</title> | ||||
| </head> | ||||
| <body> | ||||
|   <div id="splash-mount"></div> | ||||
|   <script src="index.js"></script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										41
									
								
								src/splash/web/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/splash/web/index.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										6
									
								
								src/splash/web/variables.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/splash/web/variables.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| { | ||||
|   "width": "300px", | ||||
|   "height": "300px", | ||||
|   "inDuration": 700, | ||||
|   "outDuration": 333 | ||||
| } | ||||
							
								
								
									
										73
									
								
								src/updater/appUpdater.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/updater/appUpdater.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.update = update; | ||||
| exports.focusSplash = focusSplash; | ||||
| 
 | ||||
| var _fs = _interopRequireDefault(require("fs")); | ||||
| 
 | ||||
| var _path = _interopRequireDefault(require("path")); | ||||
| 
 | ||||
| var moduleUpdater = _interopRequireWildcard(require("./moduleUpdater")); | ||||
| 
 | ||||
| var paths = _interopRequireWildcard(require("../paths")); | ||||
| 
 | ||||
| var _updater = require("./updater"); | ||||
| 
 | ||||
| var _appSettings = require("../appSettings"); | ||||
| 
 | ||||
| var autoStart = _interopRequireWildcard(require("../autoStart")); | ||||
| 
 | ||||
| var _buildInfo = _interopRequireDefault(require("../utils/buildInfo")); | ||||
| 
 | ||||
| var _errorHandler = require("../errorHandler"); | ||||
| 
 | ||||
| var firstRun = _interopRequireWildcard(require("../firstRun")); | ||||
| 
 | ||||
| var splashScreen = _interopRequireWildcard(require("../splash/splashScreen")); | ||||
| 
 | ||||
| var _Constants = require("../Constants"); | ||||
| 
 | ||||
| function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||||
| 
 | ||||
| function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| // settings
 | ||||
| const USE_PINNED_UPDATE_MANIFEST = 'USE_PINNED_UPDATE_MANIFEST'; | ||||
| 
 | ||||
| function update(startMinimized, doneCallback, showCallback) { | ||||
|   const settings = (0, _appSettings.getSettings)(); | ||||
| 
 | ||||
|   if ((0, _updater.tryInitUpdater)(_buildInfo.default, _Constants.NEW_UPDATE_ENDPOINT)) { | ||||
|     const updater = (0, _updater.getUpdater)(); | ||||
|     const usePinnedUpdateManifest = settings.get(USE_PINNED_UPDATE_MANIFEST); | ||||
|     updater.on('host-updated', () => { | ||||
|       autoStart.update(() => {}); | ||||
|     }); | ||||
|     updater.on('unhandled-exception', _errorHandler.fatal); | ||||
|     updater.on(_updater.INCONSISTENT_INSTALLER_STATE_ERROR, _errorHandler.fatal); | ||||
|     updater.on('update-error', _errorHandler.handled); | ||||
| 
 | ||||
|     if (usePinnedUpdateManifest) { | ||||
|       const manifestPath = _path.default.join(paths.getUserData(), 'pinned_update.json'); | ||||
| 
 | ||||
|       updater.setPinnedManifestSync(JSON.parse(_fs.default.readFileSync(manifestPath))); | ||||
|     } | ||||
| 
 | ||||
|     firstRun.performFirstRunTasks(updater); | ||||
|   } else { | ||||
|     moduleUpdater.init(_Constants.UPDATE_ENDPOINT, settings, _buildInfo.default); | ||||
|   } | ||||
| 
 | ||||
|   splashScreen.initSplash(startMinimized); | ||||
|   splashScreen.events.once(splashScreen.APP_SHOULD_LAUNCH, doneCallback); | ||||
|   splashScreen.events.once(splashScreen.APP_SHOULD_SHOW, showCallback); | ||||
| } | ||||
| 
 | ||||
| function focusSplash() { | ||||
|   splashScreen.focusWindow(); | ||||
| } | ||||
							
								
								
									
										199
									
								
								src/updater/hostUpdater.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								src/updater/hostUpdater.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,199 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.default = void 0; | ||||
| 
 | ||||
| var _electron = require("electron"); | ||||
| 
 | ||||
| var _events = require("events"); | ||||
| 
 | ||||
| var _request = _interopRequireDefault(require("./request")); | ||||
| 
 | ||||
| var squirrelUpdate = _interopRequireWildcard(require("./squirrelUpdate")); | ||||
| 
 | ||||
| function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||||
| 
 | ||||
| function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| function versionParse(verString) { | ||||
|   return verString.split('.').map(i => parseInt(i)); | ||||
| } | ||||
| 
 | ||||
| function versionNewer(verA, verB) { | ||||
|   let i = 0; | ||||
| 
 | ||||
|   while (true) { | ||||
|     const a = verA[i]; | ||||
|     const b = verB[i]; | ||||
|     i++; | ||||
| 
 | ||||
|     if (a === undefined) { | ||||
|       return false; | ||||
|     } else { | ||||
|       if (b === undefined || a > b) { | ||||
|         return true; | ||||
|       } | ||||
| 
 | ||||
|       if (a < b) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class AutoUpdaterWin32 extends _events.EventEmitter { | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.updateUrl = null; | ||||
|     this.updateVersion = null; | ||||
|   } | ||||
| 
 | ||||
|   setFeedURL(updateUrl) { | ||||
|     this.updateUrl = updateUrl; | ||||
|   } | ||||
| 
 | ||||
|   quitAndInstall() { | ||||
|     if (squirrelUpdate.updateExistsSync()) { | ||||
|       squirrelUpdate.restart(_electron.app, this.updateVersion || _electron.app.getVersion()); | ||||
|     } else { | ||||
|       require('auto-updater').quitAndInstall(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   downloadAndInstallUpdate(callback) { | ||||
|     squirrelUpdate.spawnUpdateInstall(this.updateUrl, progress => { | ||||
|       this.emit('update-progress', progress); | ||||
|     }).catch(err => callback(err)).then(() => callback()); | ||||
|   } | ||||
| 
 | ||||
|   checkForUpdates() { | ||||
|     if (this.updateUrl == null) { | ||||
|       throw new Error('Update URL is not set'); | ||||
|     } | ||||
| 
 | ||||
|     this.emit('checking-for-update'); | ||||
| 
 | ||||
|     if (!squirrelUpdate.updateExistsSync()) { | ||||
|       this.emit('update-not-available'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     squirrelUpdate.spawnUpdate(['--check', this.updateUrl], (error, stdout) => { | ||||
|       if (error != null) { | ||||
|         this.emit('error', error); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       try { | ||||
|         // Last line of the output is JSON details about the releases
 | ||||
|         const json = stdout.trim().split('\n').pop(); | ||||
|         const releasesFound = JSON.parse(json).releasesToApply; | ||||
| 
 | ||||
|         if (releasesFound == null || releasesFound.length == 0) { | ||||
|           this.emit('update-not-available'); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         const update = releasesFound.pop(); | ||||
|         this.emit('update-available'); | ||||
|         this.downloadAndInstallUpdate(error => { | ||||
|           if (error != null) { | ||||
|             this.emit('error', error); | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           this.updateVersion = update.version; | ||||
|           this.emit('update-downloaded', {}, update.release, update.version, new Date(), this.updateUrl, this.quitAndInstall.bind(this)); | ||||
|         }); | ||||
|       } catch (error) { | ||||
|         error.stdout = stdout; | ||||
|         this.emit('error', error); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| } // todo
 | ||||
| 
 | ||||
| 
 | ||||
| class AutoUpdaterLinux extends _events.EventEmitter { | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.updateUrl = null; | ||||
|   } | ||||
| 
 | ||||
|   setFeedURL(url) { | ||||
|     this.updateUrl = url; | ||||
|   } | ||||
| 
 | ||||
|   quitAndInstall() { | ||||
|     // Just restart. The splash screen will hit the update manually state and
 | ||||
|     // prompt the user to download the new package.
 | ||||
|     _electron.app.relaunch(); | ||||
| 
 | ||||
|     _electron.app.quit(); | ||||
|   } | ||||
| 
 | ||||
|   async checkForUpdates() { | ||||
|     const currVersion = versionParse(_electron.app.getVersion()); | ||||
|     this.emit('checking-for-update'); | ||||
| 
 | ||||
|     try { | ||||
|       const response = await _request.default.get(this.updateUrl); | ||||
| 
 | ||||
|       if (response.statusCode === 204) { | ||||
|         // you are up to date
 | ||||
|         this.emit('update-not-available'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       let latestVerStr = ''; | ||||
|       let latestVersion = []; | ||||
| 
 | ||||
|       try { | ||||
|         const latestMetadata = JSON.parse(response.body); | ||||
|         latestVerStr = latestMetadata.name; | ||||
|         latestVersion = versionParse(latestVerStr); | ||||
|       } catch (_) {} | ||||
| 
 | ||||
|       if (versionNewer(latestVersion, currVersion)) { | ||||
|         console.log('[Updates] You are out of date!'); // you need to update
 | ||||
| 
 | ||||
|         this.emit('update-manually', latestVerStr); | ||||
|       } else { | ||||
|         console.log('[Updates] You are living in the future!'); | ||||
|         this.emit('update-not-available'); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.error('[Updates] Error fetching ' + this.updateUrl + ': ' + err.message); | ||||
|       this.emit('error', err); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| let autoUpdater; // TODO
 | ||||
| // events: checking-for-update, update-available, update-not-available, update-manually, update-downloaded, error
 | ||||
| // also, checkForUpdates, setFeedURL, quitAndInstall
 | ||||
| // also, see electron.autoUpdater, and its API
 | ||||
| 
 | ||||
| switch (process.platform) { | ||||
|   case 'darwin': | ||||
|     autoUpdater = require('electron').autoUpdater; | ||||
|     break; | ||||
| 
 | ||||
|   case 'win32': | ||||
|     autoUpdater = new AutoUpdaterWin32(); | ||||
|     break; | ||||
| 
 | ||||
|   case 'linux': | ||||
|     autoUpdater = new AutoUpdaterLinux(); | ||||
|     break; | ||||
| } | ||||
| 
 | ||||
| var _default = autoUpdater; | ||||
| exports.default = _default; | ||||
| module.exports = exports.default; | ||||
							
								
								
									
										967
									
								
								src/updater/moduleUpdater.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										967
									
								
								src/updater/moduleUpdater.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,967 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.initPathsOnly = initPathsOnly; | ||||
| exports.init = init; | ||||
| exports.checkForUpdates = checkForUpdates; | ||||
| exports.setInBackground = setInBackground; | ||||
| exports.quitAndInstallUpdates = quitAndInstallUpdates; | ||||
| exports.isInstalled = isInstalled; | ||||
| exports.getInstalled = getInstalled; | ||||
| exports.install = install; | ||||
| exports.installPendingUpdates = installPendingUpdates; | ||||
| exports.supportsEventObjects = exports.events = exports.NO_PENDING_UPDATES = exports.INSTALLING_MODULE_PROGRESS = exports.INSTALLING_MODULE = exports.INSTALLING_MODULES_FINISHED = exports.DOWNLOADED_MODULE = exports.UPDATE_MANUALLY = exports.DOWNLOADING_MODULES_FINISHED = exports.DOWNLOADING_MODULE_PROGRESS = exports.DOWNLOADING_MODULE = exports.UPDATE_CHECK_FINISHED = exports.INSTALLED_MODULE = exports.CHECKING_FOR_UPDATES = void 0; | ||||
| 
 | ||||
| var _fs = _interopRequireDefault(require("fs")); | ||||
| 
 | ||||
| var _path = _interopRequireDefault(require("path")); | ||||
| 
 | ||||
| var _module = _interopRequireDefault(require("module")); | ||||
| 
 | ||||
| var _events = require("events"); | ||||
| 
 | ||||
| var _mkdirp = _interopRequireDefault(require("mkdirp")); | ||||
| 
 | ||||
| var _process = require("process"); | ||||
| 
 | ||||
| var _yauzl = _interopRequireDefault(require("yauzl")); | ||||
| 
 | ||||
| var _Backoff = _interopRequireDefault(require("../utils/Backoff")); | ||||
| 
 | ||||
| var paths = _interopRequireWildcard(require("../paths")); | ||||
| 
 | ||||
| function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||||
| 
 | ||||
| function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| // Manages additional module installation and management.
 | ||||
| // We add the module folder path to require() lookup paths here.
 | ||||
| // undocumented node API
 | ||||
| const originalFs = require('original-fs'); // events
 | ||||
| 
 | ||||
| 
 | ||||
| const CHECKING_FOR_UPDATES = 'checking-for-updates'; | ||||
| exports.CHECKING_FOR_UPDATES = CHECKING_FOR_UPDATES; | ||||
| const INSTALLED_MODULE = 'installed-module'; | ||||
| exports.INSTALLED_MODULE = INSTALLED_MODULE; | ||||
| const UPDATE_CHECK_FINISHED = 'update-check-finished'; | ||||
| exports.UPDATE_CHECK_FINISHED = UPDATE_CHECK_FINISHED; | ||||
| const DOWNLOADING_MODULE = 'downloading-module'; | ||||
| exports.DOWNLOADING_MODULE = DOWNLOADING_MODULE; | ||||
| const DOWNLOADING_MODULE_PROGRESS = 'downloading-module-progress'; | ||||
| exports.DOWNLOADING_MODULE_PROGRESS = DOWNLOADING_MODULE_PROGRESS; | ||||
| const DOWNLOADING_MODULES_FINISHED = 'downloading-modules-finished'; | ||||
| exports.DOWNLOADING_MODULES_FINISHED = DOWNLOADING_MODULES_FINISHED; | ||||
| const UPDATE_MANUALLY = 'update-manually'; | ||||
| exports.UPDATE_MANUALLY = UPDATE_MANUALLY; | ||||
| const DOWNLOADED_MODULE = 'downloaded-module'; | ||||
| exports.DOWNLOADED_MODULE = DOWNLOADED_MODULE; | ||||
| const INSTALLING_MODULES_FINISHED = 'installing-modules-finished'; | ||||
| exports.INSTALLING_MODULES_FINISHED = INSTALLING_MODULES_FINISHED; | ||||
| const INSTALLING_MODULE = 'installing-module'; | ||||
| exports.INSTALLING_MODULE = INSTALLING_MODULE; | ||||
| const INSTALLING_MODULE_PROGRESS = 'installing-module-progress'; | ||||
| exports.INSTALLING_MODULE_PROGRESS = INSTALLING_MODULE_PROGRESS; | ||||
| const NO_PENDING_UPDATES = 'no-pending-updates'; // settings
 | ||||
| 
 | ||||
| exports.NO_PENDING_UPDATES = NO_PENDING_UPDATES; | ||||
| const ALWAYS_ALLOW_UPDATES = 'ALWAYS_ALLOW_UPDATES'; | ||||
| const SKIP_HOST_UPDATE = 'SKIP_HOST_UPDATE'; | ||||
| const SKIP_MODULE_UPDATE = 'SKIP_MODULE_UPDATE'; | ||||
| const ALWAYS_BOOTSTRAP_MODULES = 'ALWAYS_BOOTSTRAP_MODULES'; | ||||
| const USE_LOCAL_MODULE_VERSIONS = 'USE_LOCAL_MODULE_VERSIONS'; | ||||
| 
 | ||||
| class Events extends _events.EventEmitter { | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.history = []; | ||||
|   } | ||||
| 
 | ||||
|   append(evt) { | ||||
|     evt.now = String(_process.hrtime.bigint()); | ||||
| 
 | ||||
|     if (this._eventIsInteresting(evt)) { | ||||
|       this.history.push(evt); | ||||
|     } | ||||
| 
 | ||||
|     process.nextTick(() => this.emit(evt.type, evt)); | ||||
|   } | ||||
| 
 | ||||
|   _eventIsInteresting(evt) { | ||||
|     return evt.type !== DOWNLOADING_MODULE_PROGRESS && evt.type !== INSTALLING_MODULE_PROGRESS; | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class LogStream { | ||||
|   constructor(logPath) { | ||||
|     try { | ||||
|       this.logStream = _fs.default.createWriteStream(logPath, { | ||||
|         flags: 'a' | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       console.error(`Failed to create ${logPath}: ${String(e)}`); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   log(message) { | ||||
|     message = `[Modules] ${message}`; | ||||
|     console.log(message); | ||||
| 
 | ||||
|     if (this.logStream) { | ||||
|       this.logStream.write(message); | ||||
|       this.logStream.write('\r\n'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   end() { | ||||
|     if (this.logStream) { | ||||
|       this.logStream.end(); | ||||
|       this.logStream = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| const request = require('./request'); | ||||
| 
 | ||||
| const REQUEST_TIMEOUT = 15000; | ||||
| const backoff = new _Backoff.default(1000, 20000); | ||||
| const events = new Events(); | ||||
| exports.events = events; | ||||
| const supportsEventObjects = true; | ||||
| exports.supportsEventObjects = supportsEventObjects; | ||||
| let logger; | ||||
| let locallyInstalledModules; | ||||
| let moduleInstallPath; | ||||
| let installedModulesFilePath; | ||||
| let moduleDownloadPath; | ||||
| let bootstrapping; | ||||
| let hostUpdater; | ||||
| let hostUpdateAvailable; | ||||
| let skipHostUpdate; | ||||
| let skipModuleUpdate; | ||||
| let checkingForUpdates; | ||||
| let remoteBaseURL; | ||||
| let remoteQuery; | ||||
| let settings; | ||||
| let remoteModuleVersions; | ||||
| let installedModules; | ||||
| let download; | ||||
| let unzip; | ||||
| let newInstallInProgress; | ||||
| let localModuleVersionsFilePath; | ||||
| let updatable; | ||||
| let bootstrapManifestFilePath; | ||||
| let runningInBackground = false; | ||||
| 
 | ||||
| function initPathsOnly(_buildInfo) { | ||||
|   if (locallyInstalledModules || moduleInstallPath) { | ||||
|     return; | ||||
|   } // If we have `localModulesRoot` in our buildInfo file, we do not fetch modules
 | ||||
|   // from remote, and rely on our locally bundled ones.
 | ||||
|   // Typically used for development mode, or private builds.
 | ||||
| 
 | ||||
| 
 | ||||
|   locallyInstalledModules = _buildInfo.localModulesRoot != null; | ||||
| 
 | ||||
|   if (locallyInstalledModules) { | ||||
|     if (_module.default.globalPaths.indexOf(_buildInfo.localModulesRoot) === -1) { | ||||
|       _module.default.globalPaths.push(_buildInfo.localModulesRoot); | ||||
|     } | ||||
|   } else { | ||||
|     moduleInstallPath = _path.default.join(paths.getUserDataVersioned(), 'modules'); | ||||
| 
 | ||||
|     if (_module.default.globalPaths.indexOf(moduleInstallPath) === -1) { | ||||
|       _module.default.globalPaths.push(moduleInstallPath); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function init(_endpoint, _settings, _buildInfo) { | ||||
|   const endpoint = _endpoint; | ||||
|   settings = _settings; | ||||
|   const buildInfo = _buildInfo; | ||||
|   updatable = buildInfo.version != '0.0.0' && !buildInfo.debug || settings.get(ALWAYS_ALLOW_UPDATES); | ||||
|   initPathsOnly(buildInfo); | ||||
|   logger = new LogStream(_path.default.join(paths.getUserData(), 'modules.log')); | ||||
|   bootstrapping = false; | ||||
|   hostUpdateAvailable = false; | ||||
|   checkingForUpdates = false; | ||||
|   skipHostUpdate = settings.get(SKIP_HOST_UPDATE) || !updatable; | ||||
|   skipModuleUpdate = settings.get(SKIP_MODULE_UPDATE) || locallyInstalledModules || !updatable; | ||||
|   localModuleVersionsFilePath = _path.default.join(paths.getUserData(), 'local_module_versions.json'); | ||||
|   bootstrapManifestFilePath = _path.default.join(paths.getResources(), 'bootstrap', 'manifest.json'); | ||||
|   installedModules = {}; | ||||
|   remoteModuleVersions = {}; | ||||
|   newInstallInProgress = {}; | ||||
|   download = { | ||||
|     // currently downloading
 | ||||
|     active: false, | ||||
|     // {name, version}
 | ||||
|     queue: [], | ||||
|     // current queue index being downloaded
 | ||||
|     next: 0, | ||||
|     // download failure count
 | ||||
|     failures: 0 | ||||
|   }; | ||||
|   unzip = { | ||||
|     // currently unzipping
 | ||||
|     active: false, | ||||
|     // {name, version, zipfile}
 | ||||
|     queue: [], | ||||
|     // current queue index being unzipped
 | ||||
|     next: 0, | ||||
|     // unzip failure count
 | ||||
|     failures: 0 | ||||
|   }; | ||||
|   logger.log(`Modules initializing`); | ||||
|   logger.log(`Distribution: ${locallyInstalledModules ? 'local' : 'remote'}`); | ||||
|   logger.log(`Host updates: ${skipHostUpdate ? 'disabled' : 'enabled'}`); | ||||
|   logger.log(`Module updates: ${skipModuleUpdate ? 'disabled' : 'enabled'}`); | ||||
| 
 | ||||
|   if (!locallyInstalledModules) { | ||||
|     installedModulesFilePath = _path.default.join(moduleInstallPath, 'installed.json'); | ||||
|     moduleDownloadPath = _path.default.join(moduleInstallPath, 'pending'); | ||||
| 
 | ||||
|     _mkdirp.default.sync(moduleDownloadPath); | ||||
| 
 | ||||
|     logger.log(`Module install path: ${moduleInstallPath}`); | ||||
|     logger.log(`Module installed file path: ${installedModulesFilePath}`); | ||||
|     logger.log(`Module download path: ${moduleDownloadPath}`); | ||||
|     let failedLoadingInstalledModules = false; | ||||
| 
 | ||||
|     try { | ||||
|       installedModules = JSON.parse(_fs.default.readFileSync(installedModulesFilePath)); | ||||
|     } catch (err) { | ||||
|       failedLoadingInstalledModules = true; | ||||
|     } | ||||
| 
 | ||||
|     cleanDownloadedModules(installedModules); | ||||
|     bootstrapping = failedLoadingInstalledModules || settings.get(ALWAYS_BOOTSTRAP_MODULES); | ||||
|   } | ||||
| 
 | ||||
|   hostUpdater = require('./hostUpdater'); // TODO: hostUpdater constants
 | ||||
| 
 | ||||
|   hostUpdater.on('checking-for-update', () => events.append({ | ||||
|     type: CHECKING_FOR_UPDATES | ||||
|   })); | ||||
|   hostUpdater.on('update-available', () => hostOnUpdateAvailable()); | ||||
|   hostUpdater.on('update-progress', progress => hostOnUpdateProgress(progress)); | ||||
|   hostUpdater.on('update-not-available', () => hostOnUpdateNotAvailable()); | ||||
|   hostUpdater.on('update-manually', newVersion => hostOnUpdateManually(newVersion)); | ||||
|   hostUpdater.on('update-downloaded', () => hostOnUpdateDownloaded()); | ||||
|   hostUpdater.on('error', err => hostOnError(err)); | ||||
|   const setFeedURL = hostUpdater.setFeedURL.bind(hostUpdater); | ||||
|   remoteBaseURL = `${endpoint}/modules/${buildInfo.releaseChannel}`; // eslint-disable-next-line camelcase
 | ||||
| 
 | ||||
|   remoteQuery = { | ||||
|     host_version: buildInfo.version | ||||
|   }; | ||||
| 
 | ||||
|   switch (process.platform) { | ||||
|     case 'darwin': | ||||
|       setFeedURL(`${endpoint}/updates/${buildInfo.releaseChannel}?platform=osx&version=${buildInfo.version}`); | ||||
|       remoteQuery.platform = 'osx'; | ||||
|       break; | ||||
| 
 | ||||
|     case 'win32': | ||||
|       // Squirrel for Windows can't handle query params
 | ||||
|       // https://github.com/Squirrel/Squirrel.Windows/issues/132
 | ||||
|       setFeedURL(`${endpoint}/updates/${buildInfo.releaseChannel}`); | ||||
|       remoteQuery.platform = 'win'; | ||||
|       break; | ||||
| 
 | ||||
|     case 'linux': | ||||
|       setFeedURL(`${endpoint}/updates/${buildInfo.releaseChannel}?platform=linux&version=${buildInfo.version}`); | ||||
|       remoteQuery.platform = 'linux'; | ||||
|       break; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function cleanDownloadedModules(installedModules) { | ||||
|   try { | ||||
|     const entries = _fs.default.readdirSync(moduleDownloadPath) || []; | ||||
|     entries.forEach(entry => { | ||||
|       const entryPath = _path.default.join(moduleDownloadPath, entry); | ||||
| 
 | ||||
|       let isStale = true; | ||||
| 
 | ||||
|       for (const moduleName of Object.keys(installedModules)) { | ||||
|         if (entryPath === installedModules[moduleName].updateZipfile) { | ||||
|           isStale = false; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (isStale) { | ||||
|         _fs.default.unlinkSync(_path.default.join(moduleDownloadPath, entry)); | ||||
|       } | ||||
|     }); | ||||
|   } catch (err) { | ||||
|     logger.log('Could not clean downloaded modules'); | ||||
|     logger.log(err.stack); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function hostOnUpdateAvailable() { | ||||
|   logger.log(`Host update is available.`); | ||||
|   hostUpdateAvailable = true; | ||||
|   events.append({ | ||||
|     type: UPDATE_CHECK_FINISHED, | ||||
|     succeeded: true, | ||||
|     updateCount: 1, | ||||
|     manualRequired: false | ||||
|   }); | ||||
|   events.append({ | ||||
|     type: DOWNLOADING_MODULE, | ||||
|     name: 'host', | ||||
|     current: 1, | ||||
|     total: 1, | ||||
|     foreground: !runningInBackground | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function hostOnUpdateProgress(progress) { | ||||
|   logger.log(`Host update progress: ${progress}%`); | ||||
|   events.append({ | ||||
|     type: DOWNLOADING_MODULE_PROGRESS, | ||||
|     name: 'host', | ||||
|     progress: progress | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function hostOnUpdateNotAvailable() { | ||||
|   logger.log(`Host is up to date.`); | ||||
| 
 | ||||
|   if (!skipModuleUpdate) { | ||||
|     checkForModuleUpdates(); | ||||
|   } else { | ||||
|     events.append({ | ||||
|       type: UPDATE_CHECK_FINISHED, | ||||
|       succeeded: true, | ||||
|       updateCount: 0, | ||||
|       manualRequired: false | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function hostOnUpdateManually(newVersion) { | ||||
|   logger.log(`Host update is available. Manual update required!`); | ||||
|   hostUpdateAvailable = true; | ||||
|   checkingForUpdates = false; | ||||
|   events.append({ | ||||
|     type: UPDATE_MANUALLY, | ||||
|     newVersion: newVersion | ||||
|   }); | ||||
|   events.append({ | ||||
|     type: UPDATE_CHECK_FINISHED, | ||||
|     succeeded: true, | ||||
|     updateCount: 1, | ||||
|     manualRequired: true | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function hostOnUpdateDownloaded() { | ||||
|   logger.log(`Host update downloaded.`); | ||||
|   checkingForUpdates = false; | ||||
|   events.append({ | ||||
|     type: DOWNLOADED_MODULE, | ||||
|     name: 'host', | ||||
|     current: 1, | ||||
|     total: 1, | ||||
|     succeeded: true | ||||
|   }); | ||||
|   events.append({ | ||||
|     type: DOWNLOADING_MODULES_FINISHED, | ||||
|     succeeded: 1, | ||||
|     failed: 0 | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function hostOnError(err) { | ||||
|   logger.log(`Host update failed: ${err}`); // [adill] osx unsigned builds will fire this code signing error inside setFeedURL and
 | ||||
|   // if we don't do anything about it hostUpdater.checkForUpdates() will never respond.
 | ||||
| 
 | ||||
|   if (err && String(err).indexOf('Could not get code signature for running application') !== -1) { | ||||
|     console.warn('Skipping host updates due to code signing failure.'); | ||||
|     skipHostUpdate = true; | ||||
|   } | ||||
| 
 | ||||
|   checkingForUpdates = false; | ||||
| 
 | ||||
|   if (!hostUpdateAvailable) { | ||||
|     events.append({ | ||||
|       type: UPDATE_CHECK_FINISHED, | ||||
|       succeeded: false, | ||||
|       updateCount: 0, | ||||
|       manualRequired: false | ||||
|     }); | ||||
|   } else { | ||||
|     events.append({ | ||||
|       type: DOWNLOADED_MODULE, | ||||
|       name: 'host', | ||||
|       current: 1, | ||||
|       total: 1, | ||||
|       succeeded: false | ||||
|     }); | ||||
|     events.append({ | ||||
|       type: DOWNLOADING_MODULES_FINISHED, | ||||
|       succeeded: 0, | ||||
|       failed: 1 | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function checkForUpdates() { | ||||
|   if (checkingForUpdates) return; | ||||
|   checkingForUpdates = true; | ||||
|   hostUpdateAvailable = false; | ||||
| 
 | ||||
|   if (skipHostUpdate) { | ||||
|     events.append({ | ||||
|       type: CHECKING_FOR_UPDATES | ||||
|     }); | ||||
|     hostOnUpdateNotAvailable(); | ||||
|   } else { | ||||
|     logger.log('Checking for host updates.'); | ||||
|     hostUpdater.checkForUpdates(); | ||||
|   } | ||||
| } // Indicates that the initial update process is complete and that future updates
 | ||||
| // are background updates. This merely affects the content of the events sent to
 | ||||
| // the app so that analytics can correctly attribute module download/installs
 | ||||
| // depending on whether they were ui-blocking or not.
 | ||||
| 
 | ||||
| 
 | ||||
| function setInBackground() { | ||||
|   runningInBackground = true; | ||||
| } | ||||
| 
 | ||||
| function getRemoteModuleName(name) { | ||||
|   if (process.platform === 'win32' && process.arch === 'x64') { | ||||
|     return `${name}.x64`; | ||||
|   } | ||||
| 
 | ||||
|   return name; | ||||
| } | ||||
| 
 | ||||
| async function checkForModuleUpdates() { | ||||
|   const query = { ...remoteQuery, | ||||
|     _: Math.floor(Date.now() / 1000 / 60 / 5) | ||||
|   }; | ||||
|   const url = `${remoteBaseURL}/versions.json`; | ||||
|   logger.log(`Checking for module updates at ${url}`); | ||||
|   let response; | ||||
| 
 | ||||
|   try { | ||||
|     response = await request.get({ | ||||
|       url, | ||||
|       qs: query, | ||||
|       timeout: REQUEST_TIMEOUT | ||||
|     }); | ||||
|     checkingForUpdates = false; | ||||
|   } catch (err) { | ||||
|     checkingForUpdates = false; | ||||
|     logger.log(`Failed fetching module versions: ${String(err)}`); | ||||
|     events.append({ | ||||
|       type: UPDATE_CHECK_FINISHED, | ||||
|       succeeded: false, | ||||
|       updateCount: 0, | ||||
|       manualRequired: false | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   remoteModuleVersions = JSON.parse(response.body); | ||||
| 
 | ||||
|   if (settings.get(USE_LOCAL_MODULE_VERSIONS)) { | ||||
|     try { | ||||
|       remoteModuleVersions = JSON.parse(_fs.default.readFileSync(localModuleVersionsFilePath)); | ||||
|       console.log('Using local module versions: ', remoteModuleVersions); | ||||
|     } catch (err) { | ||||
|       console.warn('Failed to parse local module versions: ', err); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const updatesToDownload = []; | ||||
| 
 | ||||
|   for (const moduleName of Object.keys(installedModules)) { | ||||
|     const installedModule = installedModules[moduleName]; | ||||
|     const installed = installedModule.installedVersion; | ||||
| 
 | ||||
|     if (installed === null) { | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     const update = installedModule.updateVersion || 0; | ||||
|     const remote = remoteModuleVersions[getRemoteModuleName(moduleName)] || 0; | ||||
| 
 | ||||
|     if (installed !== remote && update !== remote) { | ||||
|       logger.log(`Module update available: ${moduleName}@${remote} [installed: ${installed}]`); | ||||
|       updatesToDownload.push({ | ||||
|         name: moduleName, | ||||
|         version: remote | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   events.append({ | ||||
|     type: UPDATE_CHECK_FINISHED, | ||||
|     succeeded: true, | ||||
|     updateCount: updatesToDownload.length, | ||||
|     manualRequired: false | ||||
|   }); | ||||
| 
 | ||||
|   if (updatesToDownload.length === 0) { | ||||
|     logger.log(`No module updates available.`); | ||||
|   } else { | ||||
|     updatesToDownload.forEach(e => addModuleToDownloadQueue(e.name, e.version)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function addModuleToDownloadQueue(name, version, authToken) { | ||||
|   download.queue.push({ | ||||
|     name, | ||||
|     version, | ||||
|     authToken | ||||
|   }); | ||||
|   process.nextTick(() => processDownloadQueue()); | ||||
| } | ||||
| 
 | ||||
| async function processDownloadQueue() { | ||||
|   if (download.active) return; | ||||
|   if (download.queue.length === 0) return; | ||||
|   download.active = true; | ||||
|   const queuedModule = download.queue[download.next]; | ||||
|   download.next += 1; | ||||
|   events.append({ | ||||
|     type: DOWNLOADING_MODULE, | ||||
|     name: queuedModule.name, | ||||
|     current: download.next, | ||||
|     total: download.queue.length, | ||||
|     foreground: !runningInBackground | ||||
|   }); | ||||
|   let progress = 0; | ||||
|   let receivedBytes = 0; | ||||
|   const url = `${remoteBaseURL}/${encodeURIComponent(getRemoteModuleName(queuedModule.name))}/${encodeURIComponent(queuedModule.version)}`; | ||||
|   logger.log(`Fetching ${queuedModule.name}@${queuedModule.version} from ${url}`); | ||||
|   const headers = {}; | ||||
| 
 | ||||
|   if (queuedModule.authToken) { | ||||
|     headers['Authorization'] = queuedModule.authToken; | ||||
|   } | ||||
| 
 | ||||
|   const moduleZipPath = _path.default.join(moduleDownloadPath, `${queuedModule.name}-${queuedModule.version}.zip`); | ||||
| 
 | ||||
|   const stream = _fs.default.createWriteStream(moduleZipPath); | ||||
| 
 | ||||
|   stream.on('progress', ({ | ||||
|     receivedBytes: newReceivedBytes, | ||||
|     totalBytes | ||||
|   }) => { | ||||
|     receivedBytes = newReceivedBytes; | ||||
|     const newProgress = Math.min(Math.floor(100 * (receivedBytes / totalBytes)), 100); | ||||
| 
 | ||||
|     if (progress !== newProgress) { | ||||
|       progress = newProgress; | ||||
|       logger.log(`Streaming ${queuedModule.name}@${queuedModule.version} to ${moduleZipPath}: ${progress}%`); | ||||
|       events.append({ | ||||
|         type: DOWNLOADING_MODULE_PROGRESS, | ||||
|         name: queuedModule.name, | ||||
|         progress: progress | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|   logger.log(`Streaming ${queuedModule.name}@${queuedModule.version} to ${moduleZipPath}`); | ||||
| 
 | ||||
|   try { | ||||
|     const response = await request.get({ | ||||
|       url, | ||||
|       qs: remoteQuery, | ||||
|       headers, | ||||
|       timeout: REQUEST_TIMEOUT, | ||||
|       stream | ||||
|     }); | ||||
|     finishModuleDownload(queuedModule.name, queuedModule.version, moduleZipPath, receivedBytes, response.statusCode === 200); | ||||
|   } catch (err) { | ||||
|     logger.log(`Failed fetching module ${queuedModule.name}@${queuedModule.version}: ${String(err)}`); | ||||
|     finishModuleDownload(queuedModule.name, queuedModule.version, null, receivedBytes, false); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function commitInstalledModules() { | ||||
|   const data = JSON.stringify(installedModules, null, 2); | ||||
| 
 | ||||
|   _fs.default.writeFileSync(installedModulesFilePath, data); | ||||
| } | ||||
| 
 | ||||
| function finishModuleDownload(name, version, zipfile, receivedBytes, succeeded) { | ||||
|   if (!installedModules[name]) { | ||||
|     installedModules[name] = {}; | ||||
|   } | ||||
| 
 | ||||
|   if (succeeded) { | ||||
|     installedModules[name].updateVersion = version; | ||||
|     installedModules[name].updateZipfile = zipfile; | ||||
|     commitInstalledModules(); | ||||
|   } else { | ||||
|     download.failures += 1; | ||||
|   } | ||||
| 
 | ||||
|   events.append({ | ||||
|     type: DOWNLOADED_MODULE, | ||||
|     name: name, | ||||
|     current: download.next, | ||||
|     total: download.queue.length, | ||||
|     succeeded: succeeded, | ||||
|     receivedBytes: receivedBytes | ||||
|   }); | ||||
| 
 | ||||
|   if (download.next >= download.queue.length) { | ||||
|     const successes = download.queue.length - download.failures; | ||||
|     logger.log(`Finished module downloads. [success: ${successes}] [failure: ${download.failures}]`); | ||||
|     events.append({ | ||||
|       type: DOWNLOADING_MODULES_FINISHED, | ||||
|       succeeded: successes, | ||||
|       failed: download.failures | ||||
|     }); | ||||
|     download.queue = []; | ||||
|     download.next = 0; | ||||
|     download.failures = 0; | ||||
|     download.active = false; | ||||
|   } else { | ||||
|     const continueDownloads = () => { | ||||
|       download.active = false; | ||||
|       processDownloadQueue(); | ||||
|     }; | ||||
| 
 | ||||
|     if (succeeded) { | ||||
|       backoff.succeed(); | ||||
|       process.nextTick(continueDownloads); | ||||
|     } else { | ||||
|       logger.log(`Waiting ${Math.floor(backoff.current)}ms before next download.`); | ||||
|       backoff.fail(continueDownloads); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (newInstallInProgress[name]) { | ||||
|     addModuleToUnzipQueue(name, version, zipfile); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function addModuleToUnzipQueue(name, version, zipfile) { | ||||
|   unzip.queue.push({ | ||||
|     name, | ||||
|     version, | ||||
|     zipfile | ||||
|   }); | ||||
|   process.nextTick(() => processUnzipQueue()); | ||||
| } | ||||
| 
 | ||||
| function processUnzipQueue() { | ||||
|   if (unzip.active) return; | ||||
|   if (unzip.queue.length === 0) return; | ||||
|   unzip.active = true; | ||||
|   const queuedModule = unzip.queue[unzip.next]; | ||||
|   const installedModule = installedModules[queuedModule.name]; | ||||
|   const installedVersion = installedModule != null ? installedModule.installedVersion : null; | ||||
|   unzip.next += 1; | ||||
|   events.append({ | ||||
|     type: INSTALLING_MODULE, | ||||
|     name: queuedModule.name, | ||||
|     current: unzip.next, | ||||
|     total: unzip.queue.length, | ||||
|     foreground: !runningInBackground, | ||||
|     oldVersion: installedVersion, | ||||
|     newVersion: queuedModule.version | ||||
|   }); | ||||
|   let hasErrored = false; | ||||
| 
 | ||||
|   const onError = (error, zipfile) => { | ||||
|     if (hasErrored) return; | ||||
|     hasErrored = true; | ||||
|     logger.log(`Failed installing ${queuedModule.name}@${queuedModule.version}: ${String(error)}`); | ||||
|     succeeded = false; | ||||
| 
 | ||||
|     if (zipfile) { | ||||
|       zipfile.close(); | ||||
|     } | ||||
| 
 | ||||
|     finishModuleUnzip(queuedModule, succeeded); | ||||
|   }; | ||||
| 
 | ||||
|   let succeeded = true; | ||||
| 
 | ||||
|   const extractRoot = _path.default.join(moduleInstallPath, queuedModule.name); | ||||
| 
 | ||||
|   logger.log(`Installing ${queuedModule.name}@${queuedModule.version} from ${queuedModule.zipfile}`); | ||||
| 
 | ||||
|   const processZipfile = (err, zipfile) => { | ||||
|     if (err) { | ||||
|       onError(err, null); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const totalEntries = zipfile.entryCount; | ||||
|     let processedEntries = 0; | ||||
|     zipfile.on('entry', entry => { | ||||
|       processedEntries += 1; | ||||
|       const percent = Math.min(Math.floor(processedEntries / totalEntries * 100), 100); | ||||
|       events.append({ | ||||
|         type: INSTALLING_MODULE_PROGRESS, | ||||
|         name: queuedModule.name, | ||||
|         progress: percent | ||||
|       }); // skip directories
 | ||||
| 
 | ||||
|       if (/\/$/.test(entry.fileName)) { | ||||
|         zipfile.readEntry(); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       zipfile.openReadStream(entry, (err, stream) => { | ||||
|         if (err) { | ||||
|           onError(err, zipfile); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         stream.on('error', e => onError(e, zipfile)); | ||||
|         (0, _mkdirp.default)(_path.default.join(extractRoot, _path.default.dirname(entry.fileName)), err => { | ||||
|           if (err) { | ||||
|             onError(err, zipfile); | ||||
|             return; | ||||
|           } // [adill] createWriteStream via original-fs is broken in Electron 4.0.0-beta.6 with .asar files
 | ||||
|           // so we unzip to a temporary filename and rename it afterwards
 | ||||
| 
 | ||||
| 
 | ||||
|           const tempFileName = _path.default.join(extractRoot, entry.fileName + '.tmp'); | ||||
| 
 | ||||
|           const finalFileName = _path.default.join(extractRoot, entry.fileName); | ||||
| 
 | ||||
|           const writeStream = originalFs.createWriteStream(tempFileName); | ||||
|           writeStream.on('error', e => { | ||||
|             stream.destroy(); | ||||
| 
 | ||||
|             try { | ||||
|               originalFs.unlinkSync(tempFileName); | ||||
|             } catch (err) {} | ||||
| 
 | ||||
|             onError(e, zipfile); | ||||
|           }); | ||||
|           writeStream.on('finish', () => { | ||||
|             try { | ||||
|               originalFs.unlinkSync(finalFileName); | ||||
|             } catch (err) {} | ||||
| 
 | ||||
|             try { | ||||
|               originalFs.renameSync(tempFileName, finalFileName); | ||||
|             } catch (err) { | ||||
|               onError(err, zipfile); | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|             zipfile.readEntry(); | ||||
|           }); | ||||
|           stream.pipe(writeStream); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|     zipfile.on('error', err => { | ||||
|       onError(err, zipfile); | ||||
|     }); | ||||
|     zipfile.on('end', () => { | ||||
|       if (!succeeded) return; | ||||
|       installedModules[queuedModule.name].installedVersion = queuedModule.version; | ||||
|       finishModuleUnzip(queuedModule, succeeded); | ||||
|     }); | ||||
|     zipfile.readEntry(); | ||||
|   }; | ||||
| 
 | ||||
|   try { | ||||
|     _yauzl.default.open(queuedModule.zipfile, { | ||||
|       lazyEntries: true, | ||||
|       autoClose: true | ||||
|     }, processZipfile); | ||||
|   } catch (err) { | ||||
|     onError(err, null); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function finishModuleUnzip(unzippedModule, succeeded) { | ||||
|   delete newInstallInProgress[unzippedModule.name]; | ||||
|   delete installedModules[unzippedModule.name].updateZipfile; | ||||
|   delete installedModules[unzippedModule.name].updateVersion; | ||||
|   commitInstalledModules(); | ||||
| 
 | ||||
|   if (!succeeded) { | ||||
|     unzip.failures += 1; | ||||
|   } | ||||
| 
 | ||||
|   events.append({ | ||||
|     type: INSTALLED_MODULE, | ||||
|     name: unzippedModule.name, | ||||
|     current: unzip.next, | ||||
|     total: unzip.queue.length, | ||||
|     succeeded: succeeded | ||||
|   }); | ||||
| 
 | ||||
|   if (unzip.next >= unzip.queue.length) { | ||||
|     const successes = unzip.queue.length - unzip.failures; | ||||
|     bootstrapping = false; | ||||
|     logger.log(`Finished module installations. [success: ${successes}] [failure: ${unzip.failures}]`); | ||||
|     unzip.queue = []; | ||||
|     unzip.next = 0; | ||||
|     unzip.failures = 0; | ||||
|     unzip.active = false; | ||||
|     events.append({ | ||||
|       type: INSTALLING_MODULES_FINISHED, | ||||
|       succeeded: successes, | ||||
|       failed: unzip.failures | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   process.nextTick(() => { | ||||
|     unzip.active = false; | ||||
|     processUnzipQueue(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function quitAndInstallUpdates() { | ||||
|   logger.log(`Relaunching to install ${hostUpdateAvailable ? 'host' : 'module'} updates...`); | ||||
| 
 | ||||
|   if (hostUpdateAvailable) { | ||||
|     hostUpdater.quitAndInstall(); | ||||
|   } else { | ||||
|     relaunch(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function relaunch() { | ||||
|   logger.end(); | ||||
| 
 | ||||
|   const { | ||||
|     app | ||||
|   } = require('electron'); | ||||
| 
 | ||||
|   app.relaunch(); | ||||
|   app.quit(); | ||||
| } | ||||
| 
 | ||||
| function isInstalled(name, version) { | ||||
|   const metadata = installedModules[name]; | ||||
|   if (locallyInstalledModules) return true; | ||||
| 
 | ||||
|   if (metadata && metadata.installedVersion > 0) { | ||||
|     if (!version) return true; | ||||
|     if (metadata.installedVersion === version) return true; | ||||
|   } | ||||
| 
 | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| function getInstalled() { | ||||
|   return { ...installedModules | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function install(name, defer, options) { | ||||
|   let { | ||||
|     version, | ||||
|     authToken | ||||
|   } = options || {}; | ||||
| 
 | ||||
|   if (isInstalled(name, version)) { | ||||
|     if (!defer) { | ||||
|       events.append({ | ||||
|         type: INSTALLED_MODULE, | ||||
|         name: name, | ||||
|         current: 1, | ||||
|         total: 1, | ||||
|         succeeded: true | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (newInstallInProgress[name]) return; | ||||
| 
 | ||||
|   if (!updatable) { | ||||
|     logger.log(`Not updatable; ignoring request to install ${name}...`); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (defer) { | ||||
|     if (version) { | ||||
|       throw new Error(`Cannot defer install for a specific version module (${name}, ${version})`); | ||||
|     } | ||||
| 
 | ||||
|     logger.log(`Deferred install for ${name}...`); | ||||
|     installedModules[name] = { | ||||
|       installedVersion: 0 | ||||
|     }; | ||||
|     commitInstalledModules(); | ||||
|   } else { | ||||
|     logger.log(`Starting to install ${name}...`); | ||||
| 
 | ||||
|     if (!version) { | ||||
|       version = remoteModuleVersions[name] || 0; | ||||
|     } | ||||
| 
 | ||||
|     newInstallInProgress[name] = version; | ||||
|     addModuleToDownloadQueue(name, version, authToken); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function installPendingUpdates() { | ||||
|   const updatesToInstall = []; | ||||
| 
 | ||||
|   if (bootstrapping) { | ||||
|     let modules = {}; | ||||
| 
 | ||||
|     try { | ||||
|       modules = JSON.parse(_fs.default.readFileSync(bootstrapManifestFilePath)); | ||||
|     } catch (err) {} | ||||
| 
 | ||||
|     for (const moduleName of Object.keys(modules)) { | ||||
|       installedModules[moduleName] = { | ||||
|         installedVersion: 0 | ||||
|       }; | ||||
| 
 | ||||
|       const zipfile = _path.default.join(paths.getResources(), 'bootstrap', `${moduleName}.zip`); | ||||
| 
 | ||||
|       updatesToInstall.push({ | ||||
|         moduleName, | ||||
|         update: modules[moduleName], | ||||
|         zipfile | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   for (const moduleName of Object.keys(installedModules)) { | ||||
|     const update = installedModules[moduleName].updateVersion || 0; | ||||
|     const zipfile = installedModules[moduleName].updateZipfile; | ||||
| 
 | ||||
|     if (update > 0 && zipfile != null) { | ||||
|       updatesToInstall.push({ | ||||
|         moduleName, | ||||
|         update, | ||||
|         zipfile | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (updatesToInstall.length > 0) { | ||||
|     logger.log(`${bootstrapping ? 'Bootstrapping' : 'Installing updates'}...`); | ||||
|     updatesToInstall.forEach(e => addModuleToUnzipQueue(e.moduleName, e.update, e.zipfile)); | ||||
|   } else { | ||||
|     logger.log('No updates to install'); | ||||
|     events.append({ | ||||
|       type: NO_PENDING_UPDATES | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										186
									
								
								src/updater/request.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/updater/request.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,186 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.default = void 0; | ||||
| 
 | ||||
| var _electron = require("electron"); | ||||
| 
 | ||||
| var _querystring = _interopRequireDefault(require("querystring")); | ||||
| 
 | ||||
| var _request = _interopRequireDefault(require("request")); | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| const DEFAULT_REQUEST_TIMEOUT = 30000; | ||||
| 
 | ||||
| function makeHTTPResponse({ | ||||
|   method, | ||||
|   url, | ||||
|   headers, | ||||
|   statusCode, | ||||
|   statusMessage | ||||
| }, body) { | ||||
|   return { | ||||
|     method, | ||||
|     url, | ||||
|     headers, | ||||
|     statusCode, | ||||
|     statusMessage, | ||||
|     body | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function makeHTTPStatusError(response) { | ||||
|   const err = new Error(`HTTP Error: Status Code ${response.statusCode}`); | ||||
|   err.response = response; | ||||
|   return err; | ||||
| } | ||||
| 
 | ||||
| function handleHTTPResponse(resolve, reject, response, stream) { | ||||
|   const totalBytes = parseInt(response.headers['content-length'] || 1, 10); | ||||
|   let receivedBytes = 0; | ||||
|   const chunks = []; // don't stream response if it's a failure
 | ||||
| 
 | ||||
|   if (response.statusCode >= 300) { | ||||
|     stream = null; | ||||
|   } | ||||
| 
 | ||||
|   response.on('data', chunk => { | ||||
|     if (stream != null) { | ||||
|       receivedBytes += chunk.length; | ||||
|       stream.write(chunk); | ||||
|       stream.emit('progress', { | ||||
|         totalBytes, | ||||
|         receivedBytes | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     chunks.push(chunk); | ||||
|   }); | ||||
|   response.on('end', () => { | ||||
|     if (stream != null) { | ||||
|       stream.on('finish', () => resolve(makeHTTPResponse(response, null))); | ||||
|       stream.end(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const res = makeHTTPResponse(response, Buffer.concat(chunks)); | ||||
| 
 | ||||
|     if (res.statusCode >= 300) { | ||||
|       reject(makeHTTPStatusError(res)); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     resolve(res); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function nodeRequest({ | ||||
|   method, | ||||
|   url, | ||||
|   headers, | ||||
|   qs, | ||||
|   timeout, | ||||
|   body, | ||||
|   stream | ||||
| }) { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const req = (0, _request.default)({ | ||||
|       method, | ||||
|       url, | ||||
|       qs, | ||||
|       headers, | ||||
|       followAllRedirects: true, | ||||
|       encoding: null, | ||||
|       timeout: timeout != null ? timeout : DEFAULT_REQUEST_TIMEOUT, | ||||
|       body | ||||
|     }); | ||||
|     req.on('response', response => handleHTTPResponse(resolve, reject, response, stream)); | ||||
|     req.on('error', err => reject(err)); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function electronRequest({ | ||||
|   method, | ||||
|   url, | ||||
|   headers, | ||||
|   qs, | ||||
|   timeout, | ||||
|   body, | ||||
|   stream | ||||
| }) { | ||||
|   await _electron.app.whenReady(); | ||||
| 
 | ||||
|   const { | ||||
|     net, | ||||
|     session | ||||
|   } = require('electron'); | ||||
| 
 | ||||
|   const req = net.request({ | ||||
|     method, | ||||
|     url: `${url}${qs != null ? `?${_querystring.default.stringify(qs)}` : ''}`, | ||||
|     redirect: 'follow', | ||||
|     session: session.defaultSession | ||||
|   }); | ||||
| 
 | ||||
|   if (headers != null) { | ||||
|     for (const headerKey of Object.keys(headers)) { | ||||
|       req.setHeader(headerKey, headers[headerKey]); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (body != null) { | ||||
|     req.write(body, 'utf-8'); | ||||
|   } | ||||
| 
 | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const reqTimeout = setTimeout(() => { | ||||
|       req.abort(); | ||||
|       reject(new Error(`network timeout: ${url}`)); | ||||
|     }, timeout != null ? timeout : DEFAULT_REQUEST_TIMEOUT); | ||||
|     req.on('login', (authInfo, callback) => callback()); | ||||
|     req.on('response', response => { | ||||
|       clearTimeout(reqTimeout); | ||||
|       handleHTTPResponse(resolve, reject, response, stream); | ||||
|     }); | ||||
|     req.on('error', err => { | ||||
|       clearTimeout(reqTimeout); | ||||
|       reject(err); | ||||
|     }); | ||||
|     req.end(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function requestWithMethod(method, options) { | ||||
|   if (typeof options === 'string') { | ||||
|     options = { | ||||
|       url: options | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   options = { ...options, | ||||
|     method | ||||
|   }; | ||||
| 
 | ||||
|   try { | ||||
|     return await electronRequest(options); | ||||
|   } catch (err) { | ||||
|     console.log(`Error downloading with electron net: ${err.message}`); | ||||
|     console.log('Falling back to node net library..'); | ||||
|   } | ||||
| 
 | ||||
|   return nodeRequest(options); | ||||
| } // only supports get for now, since retrying is non-idempotent and
 | ||||
| // we'd want to grovel the errors to make sure it's safe to retry
 | ||||
| 
 | ||||
| 
 | ||||
| for (const method of ['get']) { | ||||
|   requestWithMethod[method] = requestWithMethod.bind(null, method.toUpperCase()); | ||||
| } | ||||
| 
 | ||||
| var _default = requestWithMethod; | ||||
| exports.default = _default; | ||||
| module.exports = exports.default; | ||||
							
								
								
									
										218
									
								
								src/updater/squirrelUpdate.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								src/updater/squirrelUpdate.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,218 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.spawnUpdateInstall = spawnUpdateInstall; | ||||
| exports.spawnUpdate = spawnUpdate; | ||||
| exports.installProtocol = installProtocol; | ||||
| exports.handleStartupEvent = handleStartupEvent; | ||||
| exports.updateExistsSync = updateExistsSync; | ||||
| exports.restart = restart; | ||||
| 
 | ||||
| var _child_process = _interopRequireDefault(require("child_process")); | ||||
| 
 | ||||
| var _fs = _interopRequireDefault(require("fs")); | ||||
| 
 | ||||
| var _path = _interopRequireDefault(require("path")); | ||||
| 
 | ||||
| var autoStart = _interopRequireWildcard(require("../autoStart")); | ||||
| 
 | ||||
| var windowsUtils = _interopRequireWildcard(require("./windowsUtils")); | ||||
| 
 | ||||
| function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } | ||||
| 
 | ||||
| function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| // citron note: this assumes the execPath is in the format Discord/someVersion/Discord.exe
 | ||||
| const appFolder = _path.default.resolve(process.execPath, '..'); | ||||
| 
 | ||||
| const rootFolder = _path.default.resolve(appFolder, '..'); | ||||
| 
 | ||||
| const exeName = _path.default.basename(process.execPath); | ||||
| 
 | ||||
| const updateExe = _path.default.join(rootFolder, 'Update.exe'); // Specialized spawn function specifically used for spawning the updater in
 | ||||
| // update mode. Calls back with progress percentages.
 | ||||
| // Returns Promise.
 | ||||
| 
 | ||||
| 
 | ||||
| function spawnUpdateInstall(updateUrl, progressCallback) { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const proc = _child_process.default.spawn(updateExe, ['--update', updateUrl]); | ||||
| 
 | ||||
|     proc.on('error', reject); | ||||
|     proc.on('exit', code => { | ||||
|       if (code !== 0) { | ||||
|         return reject(new Error(`Update failed with exit code ${code}`)); | ||||
|       } | ||||
| 
 | ||||
|       return resolve(); | ||||
|     }); | ||||
|     let lastProgress = -1; | ||||
| 
 | ||||
|     function parseProgress() { | ||||
|       const lines = stdout.split(/\r?\n/); | ||||
|       if (lines.length === 1) return; // return the last (possibly incomplete) line to stdout for parsing again
 | ||||
| 
 | ||||
|       stdout = lines.pop(); | ||||
|       let currentProgress; | ||||
| 
 | ||||
|       for (const line of lines) { | ||||
|         if (!/^\d\d?$/.test(line)) continue; | ||||
|         const progress = Number(line); // make sure that this number is steadily increasing
 | ||||
| 
 | ||||
|         if (lastProgress > progress) continue; | ||||
|         currentProgress = progress; | ||||
|       } | ||||
| 
 | ||||
|       if (currentProgress == null) return; | ||||
|       lastProgress = currentProgress; | ||||
|       progressCallback(Math.min(currentProgress, 100)); | ||||
|     } | ||||
| 
 | ||||
|     let stdout = ''; | ||||
|     proc.stdout.on('data', chunk => { | ||||
|       stdout += String(chunk); | ||||
|       parseProgress(); | ||||
|     }); | ||||
|   }); | ||||
| } // Spawn the Update.exe with the given arguments and invoke the callback when
 | ||||
| // the command completes.
 | ||||
| 
 | ||||
| 
 | ||||
| function spawnUpdate(args, callback) { | ||||
|   windowsUtils.spawn(updateExe, args, callback); | ||||
| } // Create a desktop and start menu shortcut by using the command line API
 | ||||
| // provided by Squirrel's Update.exe
 | ||||
| 
 | ||||
| 
 | ||||
| function createShortcuts(callback, updateOnly) { | ||||
|   // move icon out to a more stable location, to keep shortcuts from breaking as much
 | ||||
|   const icoSrc = _path.default.join(appFolder, 'app.ico'); | ||||
| 
 | ||||
|   const icoDest = _path.default.join(rootFolder, 'app.ico'); | ||||
| 
 | ||||
|   let icoForTarget = icoDest; | ||||
| 
 | ||||
|   try { | ||||
|     const ico = _fs.default.readFileSync(icoSrc); | ||||
| 
 | ||||
|     _fs.default.writeFileSync(icoDest, ico); | ||||
|   } catch (e) { | ||||
|     // if we can't write there for some reason, just use the source.
 | ||||
|     icoForTarget = icoSrc; | ||||
|   } | ||||
| 
 | ||||
|   const createShortcutArgs = ['--createShortcut', exeName, '--setupIcon', icoForTarget]; | ||||
| 
 | ||||
|   if (updateOnly) { | ||||
|     createShortcutArgs.push('--updateOnly'); | ||||
|   } | ||||
| 
 | ||||
|   spawnUpdate(createShortcutArgs, callback); | ||||
| } // Add a protocol registration for this application.
 | ||||
| 
 | ||||
| 
 | ||||
| function installProtocol(protocol, callback) { | ||||
|   const queue = [['HKCU\\Software\\Classes\\' + protocol, '/ve', '/d', `URL:${protocol} Protocol`], ['HKCU\\Software\\Classes\\' + protocol, '/v', 'URL Protocol'], ['HKCU\\Software\\Classes\\' + protocol + '\\DefaultIcon', '/ve', '/d', '"' + process.execPath + '",-1'], ['HKCU\\Software\\Classes\\' + protocol + '\\shell\\open\\command', '/ve', '/d', `"${process.execPath}" --url -- "%1"`]]; | ||||
|   windowsUtils.addToRegistry(queue, callback); | ||||
| } | ||||
| 
 | ||||
| function terminate(app) { | ||||
|   app.quit(); | ||||
|   process.exit(0); | ||||
| } // Remove the desktop and start menu shortcuts by using the command line API
 | ||||
| // provided by Squirrel's Update.exe
 | ||||
| 
 | ||||
| 
 | ||||
| function removeShortcuts(callback) { | ||||
|   spawnUpdate(['--removeShortcut', exeName], callback); | ||||
| } // Update the desktop and start menu shortcuts by using the command line API
 | ||||
| // provided by Squirrel's Update.exe
 | ||||
| 
 | ||||
| 
 | ||||
| function updateShortcuts(callback) { | ||||
|   createShortcuts(callback, true); | ||||
| } // Purge the protocol for this applicationstart.
 | ||||
| 
 | ||||
| 
 | ||||
| function uninstallProtocol(protocol, callback) { | ||||
|   windowsUtils.spawnReg(['delete', 'HKCU\\Software\\Classes\\' + protocol, '/f'], callback); | ||||
| } | ||||
| 
 | ||||
| function maybeInstallNewUpdaterSeedDb() { | ||||
|   const installerDbSrc = _path.default.join(appFolder, 'installer.db'); | ||||
| 
 | ||||
|   const installerDbDest = _path.default.join(rootFolder, 'installer.db'); | ||||
| 
 | ||||
|   if (_fs.default.existsSync(installerDbSrc)) { | ||||
|     _fs.default.renameSync(installerDbSrc, installerDbDest); | ||||
|   } | ||||
| } // Handle squirrel events denoted by --squirrel-* command line arguments.
 | ||||
| // returns `true` if regular startup should be prevented
 | ||||
| 
 | ||||
| 
 | ||||
| function handleStartupEvent(protocol, app, squirrelCommand) { | ||||
|   switch (squirrelCommand) { | ||||
|     case '--squirrel-install': | ||||
|       createShortcuts(() => { | ||||
|         autoStart.install(() => { | ||||
|           installProtocol(protocol, () => { | ||||
|             // Squirrel doesn't have a way to include app-level files.
 | ||||
|             // We get around this for new updater hosts, which rely on
 | ||||
|             // a seeded manifest, by bubbling the db up from the versioned-app
 | ||||
|             // directory if it exists.
 | ||||
|             maybeInstallNewUpdaterSeedDb(); | ||||
|             terminate(app); | ||||
|           }); | ||||
|         }); | ||||
|       }, false); | ||||
|       return true; | ||||
| 
 | ||||
|     case '--squirrel-updated': | ||||
|       updateShortcuts(() => { | ||||
|         autoStart.update(() => { | ||||
|           installProtocol(protocol, () => { | ||||
|             terminate(app); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|       return true; | ||||
| 
 | ||||
|     case '--squirrel-uninstall': | ||||
|       removeShortcuts(() => { | ||||
|         autoStart.uninstall(() => { | ||||
|           uninstallProtocol(protocol, () => { | ||||
|             terminate(app); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|       return true; | ||||
| 
 | ||||
|     case '--squirrel-obsolete': | ||||
|       terminate(app); | ||||
|       return true; | ||||
| 
 | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } // Are we using Squirrel for updates?
 | ||||
| 
 | ||||
| 
 | ||||
| function updateExistsSync() { | ||||
|   return _fs.default.existsSync(updateExe); | ||||
| } // Restart app as the new version
 | ||||
| 
 | ||||
| 
 | ||||
| function restart(app, newVersion) { | ||||
|   app.once('will-quit', () => { | ||||
|     const execPath = _path.default.resolve(rootFolder, `app-${newVersion}/${exeName}`); | ||||
| 
 | ||||
|     _child_process.default.spawn(execPath, [], { | ||||
|       detached: true | ||||
|     }); | ||||
|   }); | ||||
|   app.quit(); | ||||
| } | ||||
							
								
								
									
										460
									
								
								src/updater/updater.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								src/updater/updater.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,460 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| // Too much Rust integration stuff in here.
 | ||||
| 
 | ||||
| /* eslint camelcase: 0 */ | ||||
| const childProcess = require('child_process'); | ||||
| 
 | ||||
| const { | ||||
|   app | ||||
| } = require('electron'); | ||||
| 
 | ||||
| const { | ||||
|   EventEmitter | ||||
| } = require('events'); | ||||
| 
 | ||||
| const NodeModule = require('module'); | ||||
| 
 | ||||
| const path = require('path'); | ||||
| 
 | ||||
| const { | ||||
|   hrtime | ||||
| } = require('process'); | ||||
| 
 | ||||
| let instance; | ||||
| const TASK_STATE_COMPLETE = 'Complete'; | ||||
| const TASK_STATE_FAILED = 'Failed'; | ||||
| const TASK_STATE_WAITING = 'Waiting'; | ||||
| const TASK_STATE_WORKING = 'Working'; | ||||
| const INCONSISTENT_INSTALLER_STATE_ERROR = 'InconsistentInstallerState'; // The dumb linters are mad at each other.
 | ||||
| // eslint-disable-next-line quotes
 | ||||
| 
 | ||||
| const INVALID_UPDATER_ERROR = "Can't send request to updater because the native updater isn't loaded."; | ||||
| 
 | ||||
| class Updater extends EventEmitter { | ||||
|   constructor(options) { | ||||
|     super(); | ||||
|     let nativeUpdaterModule = options.nativeUpdaterModule; | ||||
| 
 | ||||
|     if (nativeUpdaterModule == null) { | ||||
|       try { | ||||
|         // eslint-disable-next-line import/no-unresolved
 | ||||
|         nativeUpdaterModule = require('../../../updater'); | ||||
|       } catch (e) { | ||||
|         if (e.code === 'MODULE_NOT_FOUND') { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         throw e; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     this.committedHostVersion = null; | ||||
|     this.committedModules = new Set(); | ||||
|     this.rootPath = options.root_path; | ||||
|     this.nextRequestId = 0; | ||||
|     this.requests = new Map(); | ||||
|     this.updateEventHistory = []; | ||||
|     this.isRunningInBackground = false; | ||||
|     this.currentlyDownloading = {}; | ||||
|     this.currentlyInstalling = {}; | ||||
|     this.hasEmittedUnhandledException = false; | ||||
|     this.nativeUpdater = new nativeUpdaterModule.Updater({ | ||||
|       response_handler: this._handleResponse.bind(this), | ||||
|       ...options | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get valid() { | ||||
|     return this.nativeUpdater != null; | ||||
|   } | ||||
| 
 | ||||
|   _sendRequest(detail, progressCallback = null) { | ||||
|     if (!this.valid) { | ||||
|       throw new Error(INVALID_UPDATER_ERROR); | ||||
|     } | ||||
| 
 | ||||
|     const requestId = this.nextRequestId++; | ||||
|     return new Promise((resolve, reject) => { | ||||
|       this.requests.set(requestId, { | ||||
|         resolve, | ||||
|         reject, | ||||
|         progressCallback | ||||
|       }); | ||||
|       this.nativeUpdater.command(JSON.stringify([requestId, detail])); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   _sendRequestSync(detail) { | ||||
|     if (!this.valid) { | ||||
|       throw new Error(INVALID_UPDATER_ERROR); | ||||
|     } | ||||
| 
 | ||||
|     const requestId = this.nextRequestId++; | ||||
|     return this.nativeUpdater.command_blocking(JSON.stringify([requestId, detail])); | ||||
|   } | ||||
| 
 | ||||
|   _handleResponse(response) { | ||||
|     try { | ||||
|       const [id, detail] = JSON.parse(response); | ||||
|       const request = this.requests.get(id); | ||||
| 
 | ||||
|       if (request == null) { | ||||
|         console.error('Received response ', detail, ' for a request (', id, ') not in the updater request map.'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (detail['Error'] != null) { | ||||
|         const { | ||||
|           kind, | ||||
|           details, | ||||
|           severity | ||||
|         } = detail['Error']; | ||||
|         const e = new Error(`(${kind}) ${details}`); | ||||
| 
 | ||||
|         if (severity === 'Fatal') { | ||||
|           const handled = this.emit(kind, e); | ||||
| 
 | ||||
|           if (!handled) { | ||||
|             throw e; | ||||
|           } | ||||
|         } else { | ||||
|           this.emit('update-error', e); | ||||
|           request.reject(e); | ||||
|           this.requests.delete(id); | ||||
|         } | ||||
|       } else if (detail === 'Ok') { | ||||
|         request.resolve(); | ||||
|         this.requests.delete(id); | ||||
|       } else if (detail['VersionInfo'] != null) { | ||||
|         request.resolve(detail['VersionInfo']); | ||||
|         this.requests.delete(id); | ||||
|       } else if (detail['ManifestInfo'] != null) { | ||||
|         request.resolve(detail['ManifestInfo']); | ||||
|         this.requests.delete(id); | ||||
|       } else if (detail['TaskProgress'] != null) { | ||||
|         const msg = detail['TaskProgress']; | ||||
|         const progress = { | ||||
|           task: msg[0], | ||||
|           state: msg[1], | ||||
|           percent: msg[2], | ||||
|           bytesProcessed: msg[3] | ||||
|         }; | ||||
| 
 | ||||
|         this._recordTaskProgress(progress); | ||||
| 
 | ||||
|         if (request.progressCallback != null) { | ||||
|           request.progressCallback(progress); | ||||
|         } | ||||
| 
 | ||||
|         if (progress.task['HostInstall'] != null && progress.state === TASK_STATE_COMPLETE) { | ||||
|           this.emit('host-updated'); | ||||
|         } | ||||
|       } else { | ||||
|         console.warn('Unknown updater response', detail); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.error('Unhandled exception in updater response handler:', e); // Report the first time this happens, but don't spam.
 | ||||
| 
 | ||||
|       if (!this.hasEmittedUnhandledException) { | ||||
|         this.hasEmittedUnhandledException = true; | ||||
|         this.emit('unhandled-exception', e); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _handleSyncResponse(response) { | ||||
|     const detail = JSON.parse(response); | ||||
| 
 | ||||
|     if (detail['Error'] != null) { | ||||
|       throw new Error(detail['Error']); | ||||
|     } else if (detail === 'Ok') { | ||||
|       return; | ||||
|     } else if (detail['VersionInfo'] != null) { | ||||
|       return detail['VersionInfo']; | ||||
|     } | ||||
| 
 | ||||
|     console.warn('Unknown updater response', detail); | ||||
|   } | ||||
| 
 | ||||
|   _getHostPath() { | ||||
|     const [major, minor, revision] = this.committedHostVersion; | ||||
|     const hostVersionStr = `${major}.${minor}.${revision}`; | ||||
|     return path.join(this.rootPath, `app-${hostVersionStr}`); | ||||
|   } | ||||
| 
 | ||||
|   _startCurrentVersionInner(options, versions) { | ||||
|     if (this.committedHostVersion == null) { | ||||
|       this.committedHostVersion = versions.current_host; | ||||
|     } | ||||
| 
 | ||||
|     const hostPath = this._getHostPath(); | ||||
| 
 | ||||
|     const hostExePath = path.join(hostPath, path.basename(process.execPath)); | ||||
| 
 | ||||
|     if (path.resolve(hostExePath) != path.resolve(process.execPath) && !(options === null || options === void 0 ? void 0 : options.allowObsoleteHost)) { | ||||
|       app.once('will-quit', () => { | ||||
|         // TODO(eiz): the actual, correct way to do this (win32) is to inherit a
 | ||||
|         // handle to the current process into a new child process which then
 | ||||
|         // waits for that process handle to exit, then runs the new electron.
 | ||||
|         // This requires either implementing a separate updater exe process (big
 | ||||
|         // todo item atm) or likely modifying Electron?
 | ||||
|         //
 | ||||
|         // I intend to do it properly once the new production updater .exe is a
 | ||||
|         // thing.
 | ||||
|         childProcess.spawn(hostExePath, [], { | ||||
|           detached: true, | ||||
|           stdio: 'inherit' | ||||
|         }); | ||||
|       }); | ||||
|       console.log(`Restarting from ${path.resolve(process.execPath)} to ${path.resolve(hostExePath)}`); | ||||
|       app.quit(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this._commitModulesInner(versions); | ||||
|   } | ||||
| 
 | ||||
|   _commitModulesInner(versions) { | ||||
|     const hostPath = this._getHostPath(); | ||||
| 
 | ||||
|     const modulesPath = path.join(hostPath, 'modules'); | ||||
| 
 | ||||
|     for (const module in versions.current_modules) { | ||||
|       const moduleVersion = versions.current_modules[module]; | ||||
|       const moduleSearchPath = path.join(modulesPath, `${module}-${moduleVersion}`); | ||||
| 
 | ||||
|       if (!this.committedModules.has(module) && NodeModule.globalPaths.indexOf(moduleSearchPath) === -1) { | ||||
|         this.committedModules.add(module); | ||||
|         NodeModule.globalPaths.push(moduleSearchPath); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _recordDownloadProgress(name, progress) { | ||||
|     const now = String(hrtime.bigint()); | ||||
| 
 | ||||
|     if (progress.state === TASK_STATE_WORKING && !this.currentlyDownloading[name]) { | ||||
|       this.currentlyDownloading[name] = true; | ||||
|       this.updateEventHistory.push({ | ||||
|         type: 'downloading-module', | ||||
|         name: name, | ||||
|         now: now | ||||
|       }); | ||||
|     } else if (progress.state === TASK_STATE_COMPLETE || progress.state === TASK_STATE_FAILED) { | ||||
|       this.currentlyDownloading[name] = false; | ||||
|       this.updateEventHistory.push({ | ||||
|         type: 'downloaded-module', | ||||
|         name: name, | ||||
|         now: now, | ||||
|         succeeded: progress.state === TASK_STATE_COMPLETE, | ||||
|         receivedBytes: progress.bytesProcessed | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _recordInstallProgress(name, progress, newVersion, isDelta) { | ||||
|     const now = String(hrtime.bigint()); | ||||
| 
 | ||||
|     if (progress.state === TASK_STATE_WORKING && !this.currentlyInstalling[name]) { | ||||
|       this.currentlyInstalling[name] = true; | ||||
|       this.updateEventHistory.push({ | ||||
|         type: 'installing-module', | ||||
|         name, | ||||
|         now, | ||||
|         newVersion, | ||||
|         foreground: !this.isRunningInBackground | ||||
|       }); | ||||
|     } else if (progress.state === TASK_STATE_COMPLETE || progress.state === TASK_STATE_FAILED) { | ||||
|       this.currentlyInstalling[name] = false; | ||||
|       this.updateEventHistory.push({ | ||||
|         type: 'installed-module', | ||||
|         name, | ||||
|         now, | ||||
|         newVersion, | ||||
|         succeeded: progress.state === TASK_STATE_COMPLETE, | ||||
|         delta: isDelta, | ||||
|         foreground: !this.isRunningInBackground | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _recordTaskProgress(progress) { | ||||
|     if (progress.task.HostDownload != null) { | ||||
|       this._recordDownloadProgress('host', progress); | ||||
|     } else if (progress.task.HostInstall != null) { | ||||
|       this._recordInstallProgress('host', progress, null, progress.task.HostInstall.from_version != null); | ||||
|     } else if (progress.task.ModuleDownload != null) { | ||||
|       this._recordDownloadProgress(progress.task.ModuleDownload.version.module.name, progress); | ||||
|     } else if (progress.task.ModuleInstall != null) { | ||||
|       this._recordInstallProgress(progress.task.ModuleInstall.version.module.name, progress, progress.task.ModuleInstall.version.version, progress.task.ModuleInstall.from_version != null); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   queryCurrentVersions() { | ||||
|     return this._sendRequest('QueryCurrentVersions'); | ||||
|   } | ||||
| 
 | ||||
|   queryCurrentVersionsSync() { | ||||
|     return this._handleSyncResponse(this._sendRequestSync('QueryCurrentVersions')); | ||||
|   } | ||||
| 
 | ||||
|   repair(progressCallback) { | ||||
|     return this.repairWithOptions(null, progressCallback); | ||||
|   } | ||||
| 
 | ||||
|   repairWithOptions(options, progressCallback) { | ||||
|     return this._sendRequest({ | ||||
|       Repair: { | ||||
|         options | ||||
|       } | ||||
|     }, progressCallback); | ||||
|   } | ||||
| 
 | ||||
|   collectGarbage() { | ||||
|     return this._sendRequest('CollectGarbage'); | ||||
|   } | ||||
| 
 | ||||
|   setRunningManifest(manifest) { | ||||
|     return this._sendRequest({ | ||||
|       SetManifests: ['Running', manifest] | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   setPinnedManifestSync(manifest) { | ||||
|     return this._handleSyncResponse(this._sendRequestSync({ | ||||
|       SetManifests: ['Pinned', manifest] | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   installModule(name, progressCallback) { | ||||
|     return this.installModuleWithOptions(name, null, progressCallback); | ||||
|   } | ||||
| 
 | ||||
|   installModuleWithOptions(name, options, progressCallback) { | ||||
|     return this._sendRequest({ | ||||
|       InstallModule: { | ||||
|         name, | ||||
|         options | ||||
|       } | ||||
|     }, progressCallback); | ||||
|   } | ||||
| 
 | ||||
|   updateToLatest(progressCallback) { | ||||
|     return this.updateToLatestWithOptions(null, progressCallback); | ||||
|   } | ||||
| 
 | ||||
|   updateToLatestWithOptions(options, progressCallback) { | ||||
|     return this._sendRequest({ | ||||
|       UpdateToLatest: { | ||||
|         options | ||||
|       } | ||||
|     }, progressCallback); | ||||
|   } // If the running host is current, adopt the current installed modules and
 | ||||
|   // set up the module search path accordingly. If the running host is not
 | ||||
|   // current, start the new current host and exit this process.
 | ||||
| 
 | ||||
| 
 | ||||
|   async startCurrentVersion(options) { | ||||
|     const versions = await this.queryCurrentVersions(); | ||||
|     await this.setRunningManifest(versions.last_successful_update); | ||||
| 
 | ||||
|     this._startCurrentVersionInner(options, versions); | ||||
|   } | ||||
| 
 | ||||
|   startCurrentVersionSync(options) { | ||||
|     const versions = this.queryCurrentVersionsSync(); | ||||
| 
 | ||||
|     this._startCurrentVersionInner(options, versions); | ||||
|   } | ||||
| 
 | ||||
|   async commitModules(versions) { | ||||
|     if (this.committedHostVersion == null) { | ||||
|       throw new Error('Cannot commit modules before host version.'); | ||||
|     } | ||||
| 
 | ||||
|     if (versions == null) { | ||||
|       versions = await this.queryCurrentVersions(); | ||||
|     } | ||||
| 
 | ||||
|     this._commitModulesInner(versions); | ||||
|   } | ||||
| 
 | ||||
|   setRunningInBackground() { | ||||
|     this.isRunningInBackground = true; | ||||
|   } | ||||
| 
 | ||||
|   queryAndTruncateHistory() { | ||||
|     const history = this.updateEventHistory; | ||||
|     this.updateEventHistory = []; | ||||
|     return history; | ||||
|   } | ||||
| 
 | ||||
|   getKnownFolder(name) { | ||||
|     if (!this.valid) { | ||||
|       throw new Error(INVALID_UPDATER_ERROR); | ||||
|     } | ||||
| 
 | ||||
|     return this.nativeUpdater.known_folder(name); | ||||
|   } | ||||
| 
 | ||||
|   createShortcut(options) { | ||||
|     if (!this.valid) { | ||||
|       throw new Error(INVALID_UPDATER_ERROR); | ||||
|     } | ||||
| 
 | ||||
|     return this.nativeUpdater.create_shortcut(options); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| function getUpdaterPlatformName(platform) { | ||||
|   switch (platform) { | ||||
|     case 'darwin': | ||||
|       return 'osx'; | ||||
| 
 | ||||
|     case 'win32': | ||||
|       return 'win'; | ||||
| 
 | ||||
|     default: | ||||
|       return platform; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function tryInitUpdater(buildInfo, repositoryUrl) { | ||||
|   // We can't require this in module scope because it's not part of the
 | ||||
|   // bootstrapper, which carries a copy of the Updater class.
 | ||||
|   const paths = require('../paths'); | ||||
| 
 | ||||
|   const rootPath = paths.getInstallPath(); // If we're not running from an actual install directory, don't bother trying
 | ||||
|   // to initialize the updater.
 | ||||
| 
 | ||||
|   if (rootPath == null) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   instance = new Updater({ | ||||
|     release_channel: buildInfo.releaseChannel, | ||||
|     platform: getUpdaterPlatformName(process.platform), | ||||
|     repository_url: repositoryUrl, | ||||
|     root_path: rootPath | ||||
|   }); | ||||
|   return instance.valid; | ||||
| } | ||||
| 
 | ||||
| function getUpdater() { | ||||
|   if (instance != null && instance.valid) { | ||||
|     return instance; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|   Updater, | ||||
|   tryInitUpdater, | ||||
|   getUpdater, | ||||
|   TASK_STATE_COMPLETE, | ||||
|   TASK_STATE_FAILED, | ||||
|   TASK_STATE_WAITING, | ||||
|   TASK_STATE_WORKING, | ||||
|   INCONSISTENT_INSTALLER_STATE_ERROR | ||||
| }; | ||||
							
								
								
									
										82
									
								
								src/updater/windowsUtils.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/updater/windowsUtils.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,82 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.spawn = spawn; | ||||
| exports.spawnReg = spawnReg; | ||||
| exports.addToRegistry = addToRegistry; | ||||
| 
 | ||||
| var _child_process = _interopRequireDefault(require("child_process")); | ||||
| 
 | ||||
| var _path = _interopRequireDefault(require("path")); | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| const regExe = process.env.SystemRoot ? _path.default.join(process.env.SystemRoot, 'System32', 'reg.exe') : 'reg.exe'; // Spawn a command and invoke the callback when it completes with an error
 | ||||
| // and the output from standard out.
 | ||||
| 
 | ||||
| function spawn(command, args, callback) { | ||||
|   let stdout = ''; | ||||
|   let spawnedProcess; | ||||
| 
 | ||||
|   try { | ||||
|     // TODO: contrary to below, it should not throw any error
 | ||||
|     spawnedProcess = _child_process.default.spawn(command, args); | ||||
|   } catch (err) { | ||||
|     // Spawn can throw an error
 | ||||
|     process.nextTick(() => { | ||||
|       if (callback != null) { | ||||
|         callback(err, stdout); | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } // TODO: we need to specify the encoding for the data if we're going to concat it as a string
 | ||||
| 
 | ||||
| 
 | ||||
|   spawnedProcess.stdout.on('data', data => { | ||||
|     stdout += data; | ||||
|   }); | ||||
|   let err = null; // TODO: close event might not get called, we should
 | ||||
|   //       callback on error https://nodejs.org/api/child_process.html#child_process_event_error
 | ||||
| 
 | ||||
|   spawnedProcess.on('error', err => { | ||||
|     // TODO: there should always be an error
 | ||||
|     if (err != null) { | ||||
|       err = err; | ||||
|     } | ||||
|   }); // TODO: don't listen to close, but listen to exit instead
 | ||||
| 
 | ||||
|   spawnedProcess.on('close', (code, signal) => { | ||||
|     if (err === null && code !== 0) { | ||||
|       err = new Error('Command failed: ' + (signal || code)); | ||||
|     } | ||||
| 
 | ||||
|     if (err != null) { | ||||
|       err.code = err.code || code; | ||||
|       err.stdout = err.stdout || stdout; | ||||
|     } | ||||
| 
 | ||||
|     if (callback != null) { | ||||
|       callback(err, stdout); | ||||
|     } | ||||
|   }); | ||||
| } // Spawn reg.exe and callback when it completes
 | ||||
| 
 | ||||
| 
 | ||||
| function spawnReg(args, callback) { | ||||
|   return spawn(regExe, args, callback); | ||||
| } // TODO: since we're doing this one by one, we could have a more graceful way of processing the queue
 | ||||
| //       rather than mutating the array
 | ||||
| 
 | ||||
| 
 | ||||
| function addToRegistry(queue, callback) { | ||||
|   if (queue.length === 0) { | ||||
|     return callback && callback(); | ||||
|   } | ||||
| 
 | ||||
|   const args = queue.shift(); | ||||
|   args.unshift('add'); | ||||
|   args.push('/f'); | ||||
|   return spawnReg(args, () => addToRegistry(queue, callback)); | ||||
| } | ||||
							
								
								
									
										94
									
								
								src/utils/Backoff.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/utils/Backoff.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,94 @@ | |||
| class Backoff { // Heavily based on original for compat
 | ||||
|   /** | ||||
|    * Create a backoff instance can automatically backoff retries. | ||||
|    */ | ||||
|   constructor(min = 500, max = null, jitter = true) { | ||||
|     this.min = min; | ||||
|     this.max = max != null ? max : min * 10; | ||||
|     this.jitter = jitter; | ||||
|     this._current = min; | ||||
|     this._timeoutId = null; | ||||
|     this._fails = 0; | ||||
|   } | ||||
|   /** | ||||
|    * Return the number of failures. | ||||
|    */ | ||||
| 
 | ||||
| 
 | ||||
|   get fails() { | ||||
|     return this._fails; | ||||
|   } | ||||
|   /** | ||||
|    * Current backoff value in milliseconds. | ||||
|    */ | ||||
| 
 | ||||
| 
 | ||||
|   get current() { | ||||
|     return this._current; | ||||
|   } | ||||
|   /** | ||||
|    * A callback is going to fire. | ||||
|    */ | ||||
| 
 | ||||
| 
 | ||||
|   get pending() { | ||||
|     return this._timeoutId != null; | ||||
|   } | ||||
|   /** | ||||
|    * Clear any pending callbacks and reset the backoff. | ||||
|    */ | ||||
| 
 | ||||
| 
 | ||||
|   succeed() { | ||||
|     this.cancel(); | ||||
|     this._fails = 0; | ||||
|     this._current = this.min; | ||||
|   } | ||||
|   /** | ||||
|    * Increment the backoff and schedule a callback if provided. | ||||
|    */ | ||||
| 
 | ||||
| 
 | ||||
|   fail(callback) { | ||||
|     this._fails += 1; | ||||
|     let delay = this._current * 2; | ||||
| 
 | ||||
|     if (this.jitter) { | ||||
|       delay *= Math.random(); | ||||
|     } | ||||
| 
 | ||||
|     this._current = Math.min(this._current + delay, this.max); | ||||
| 
 | ||||
|     if (callback != null) { | ||||
|       if (this._timeoutId != null) { | ||||
|         throw new Error('callback already pending'); | ||||
|       } | ||||
| 
 | ||||
|       this._timeoutId = setTimeout(() => { | ||||
|         try { | ||||
|           if (callback != null) { | ||||
|             callback(); | ||||
|           } | ||||
|         } finally { | ||||
|           this._timeoutId = null; | ||||
|         } | ||||
|       }, this._current); | ||||
|     } | ||||
| 
 | ||||
|     return this._current; | ||||
|   } | ||||
|   /** | ||||
|    *  Clear any pending callbacks. | ||||
|    */ | ||||
| 
 | ||||
| 
 | ||||
|   cancel() { | ||||
|     if (this._timeoutId != null) { | ||||
|       clearTimeout(this._timeoutId); | ||||
|       this._timeoutId = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| module.exports = Backoff; | ||||
							
								
								
									
										64
									
								
								src/utils/Settings.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/utils/Settings.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| const { readFileSync, statSync, writeFileSync } = require('fs'); | ||||
| const { join } = require('path'); | ||||
| 
 | ||||
| const log = require('./log'); | ||||
| 
 | ||||
| class Settings { // Heavily based on original for compat, but simplified and tweaked
 | ||||
|   constructor(root) { | ||||
|     this.path = join(root, 'settings.json'); | ||||
| 
 | ||||
|     try { | ||||
|       this.lastSaved = readFileSync(this.path); | ||||
|       this.settings = JSON.parse(this.lastSaved); | ||||
|     } catch (e) { | ||||
|       this.lastSaved = ''; | ||||
|       this.settings = {}; | ||||
|     } | ||||
| 
 | ||||
|     this.lastModified = this.getLastModified(); | ||||
| 
 | ||||
|     log('AppSettings', 'Loaded settings.json with path', this.path, 'with settings', this.settings, 'and last modified', this.lastModified); | ||||
|   } | ||||
| 
 | ||||
|   getLastModified() { | ||||
|     try { | ||||
|       return statSync(this.path).mtime.getTime(); | ||||
|     } catch (e) { | ||||
|       return 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   get(key, defaultValue = false) { | ||||
|     return this.settings[key] || defaultValue; | ||||
|   } | ||||
| 
 | ||||
|   set(key, value) { | ||||
|     this.settings[key] = value; | ||||
|   } | ||||
| 
 | ||||
|   save() { | ||||
|     if (this.lastModified && this.lastModified !== this.getLastModified()) { | ||||
|       log('AppSettings', 'Refusing to save settings.json due to last modified date mismatch'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       const toSave = JSON.stringify(this.settings, null, 2); | ||||
| 
 | ||||
|       if (this.lastSaved != toSave) { | ||||
|         this.lastSaved = toSave; | ||||
| 
 | ||||
|         writeFileSync(this.path, toSave); | ||||
| 
 | ||||
|         this.lastModified = this.getLastModified(); | ||||
|       } | ||||
| 
 | ||||
|       log('AppSettings', 'Saved settings.json'); | ||||
|     } catch (err) { | ||||
|       log('AppSettings', 'Failed to save settings.json', err); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| module.exports = Settings; | ||||
							
								
								
									
										8
									
								
								src/utils/buildInfo.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/utils/buildInfo.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| // Discord uses require but we'll use JSON parse for vaguely more security-ish
 | ||||
| const { readFileSync } = require('fs'); | ||||
| const { join } = require('path'); | ||||
| 
 | ||||
| const buildInfoPath = join(process.resourcesPath, 'build_info.json'); | ||||
| const buildInfo = JSON.parse(readFileSync(buildInfoPath, 'utf8')); | ||||
| 
 | ||||
| module.exports = buildInfo; | ||||
							
								
								
									
										3
									
								
								src/utils/log.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utils/log.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| const rgb = (r, g, b, text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`; | ||||
| 
 | ||||
| module.exports = (area, ...args) => console.log(`[${rgb(88, 101, 242, 'OpenAsar')}${area ? ` > ${area}` : ''}]`, ...args); | ||||
							
								
								
									
										3
									
								
								src/utils/requireNative.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utils/requireNative.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| // From Discord to only require native modules like discord_desktop_core
 | ||||
| module.paths = []; | ||||
| module.exports = require; | ||||
							
								
								
									
										45
									
								
								src/utils/securityUtils.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/utils/securityUtils.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| Object.defineProperty(exports, "__esModule", { | ||||
|   value: true | ||||
| }); | ||||
| exports.saferShellOpenExternal = saferShellOpenExternal; | ||||
| exports.checkUrlOriginMatches = checkUrlOriginMatches; | ||||
| 
 | ||||
| var _electron = require("electron"); | ||||
| 
 | ||||
| var _url = _interopRequireDefault(require("url")); | ||||
| 
 | ||||
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||||
| 
 | ||||
| const BLOCKED_URL_PROTOCOLS = ['file:', 'javascript:', 'vbscript:', 'data:', 'about:', 'chrome:', 'ms-cxh:', 'ms-cxh-full:', 'ms-word:']; | ||||
| 
 | ||||
| function saferShellOpenExternal(externalUrl) { | ||||
|   let parsedUrl; | ||||
| 
 | ||||
|   try { | ||||
|     parsedUrl = _url.default.parse(externalUrl); | ||||
|   } catch (_) { | ||||
|     return Promise.reject(); | ||||
|   } | ||||
| 
 | ||||
|   if (parsedUrl.protocol == null || BLOCKED_URL_PROTOCOLS.includes(parsedUrl.protocol.toLowerCase())) { | ||||
|     return Promise.reject(); | ||||
|   } | ||||
| 
 | ||||
|   return _electron.shell.openExternal(externalUrl); | ||||
| } | ||||
| 
 | ||||
| function checkUrlOriginMatches(urlA, urlB) { | ||||
|   let parsedUrlA; | ||||
|   let parsedUrlB; | ||||
| 
 | ||||
|   try { | ||||
|     parsedUrlA = _url.default.parse(urlA); | ||||
|     parsedUrlB = _url.default.parse(urlB); | ||||
|   } catch (_) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   return parsedUrlA.protocol === parsedUrlB.protocol && parsedUrlA.slashes === parsedUrlB.slashes && parsedUrlA.host === parsedUrlB.host; | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/utils/stub.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/utils/stub.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| const log = require('./log'); | ||||
| 
 | ||||
| module.exports = (debugName) => { | ||||
|   return new Proxy({}, { | ||||
|     get(target, prop, receiver) { | ||||
|       log('Stub', `${debugName}: Tried getting ${prop}`); | ||||
|     }, | ||||
| 
 | ||||
|     set(target, prop, value, receiver) { | ||||
|       log('Stub', `${debugName}: Tried setting ${prop}, ${value}`); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										13
									
								
								test.sh
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								test.sh
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| #!/bin/sh | ||||
| 
 | ||||
| echo "Packing asar..." | ||||
| asar pack src app.asar # Package asar | ||||
| # asar list app.asar # List asar for debugging / testing | ||||
| 
 | ||||
| echo "Copying asar..." | ||||
| cp app.asar /opt/discord-canary/resources/app.asar # Overwrite app.asar for Linux Canary | ||||
| 
 | ||||
| echo "Running discord..." | ||||
| echo "" | ||||
| 
 | ||||
| discord-canary # Run it | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue