| 
									
										
										
										
											2020-10-30 15:34:04 -05:00
										 |  |  | // Copyright 2019-2020 Signal Messenger, LLC
 | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | import { dirname, join } from 'path'; | 
					
						
							|  |  |  | import { spawn as spawnEmitter, SpawnOptions } from 'child_process'; | 
					
						
							|  |  |  | import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { app, BrowserWindow } from 'electron'; | 
					
						
							| 
									
										
										
										
											2021-08-27 13:21:42 -07:00
										 |  |  | import config from 'config'; | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | import { gt } from 'semver'; | 
					
						
							|  |  |  | import pify from 'pify'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   checkForUpdates, | 
					
						
							|  |  |  |   deleteTempDir, | 
					
						
							|  |  |  |   downloadUpdate, | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |   getAutoDownloadUpdateSetting, | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   getPrintableError, | 
					
						
							| 
									
										
										
										
											2020-07-20 14:42:26 -04:00
										 |  |  |   setUpdateListener, | 
					
						
							| 
									
										
										
										
											2021-06-30 14:27:18 -07:00
										 |  |  |   UpdaterInterface, | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | } from './common'; | 
					
						
							| 
									
										
										
										
											2021-08-26 09:10:58 -05:00
										 |  |  | import * as durations from '../util/durations'; | 
					
						
							| 
									
										
										
										
											2020-04-01 11:59:11 -07:00
										 |  |  | import { LoggerType } from '../types/Logging'; | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | import { hexToBinary, verifySignature } from './signature'; | 
					
						
							|  |  |  | import { markShouldQuit } from '../../app/window_state'; | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  | import { DialogType } from '../types/Dialogs'; | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | const readdir = pify(readdirCallback); | 
					
						
							|  |  |  | const unlink = pify(unlinkCallback); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 09:10:58 -05:00
										 |  |  | const INTERVAL = 30 * durations.MINUTE; | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-20 14:42:26 -04:00
										 |  |  | let fileName: string; | 
					
						
							|  |  |  | let version: string; | 
					
						
							|  |  |  | let updateFilePath: string; | 
					
						
							|  |  |  | let installing: boolean; | 
					
						
							|  |  |  | let loggerForQuitHandler: LoggerType; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | export async function start( | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |   getMainWindow: () => BrowserWindow | undefined, | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   logger: LoggerType | 
					
						
							| 
									
										
										
										
											2021-06-30 14:27:18 -07:00
										 |  |  | ): Promise<UpdaterInterface> { | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   logger.info('windows/start: starting checks...'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   loggerForQuitHandler = logger; | 
					
						
							|  |  |  |   app.once('quit', quitHandler); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   setInterval(async () => { | 
					
						
							|  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |       await checkForUpdatesMaybeInstall(getMainWindow, logger); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |     } catch (error) { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |       logger.error(`windows/start: ${getPrintableError(error)}`); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |     } | 
					
						
							|  |  |  |   }, INTERVAL); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   await deletePreviousInstallers(logger); | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |   await checkForUpdatesMaybeInstall(getMainWindow, logger); | 
					
						
							| 
									
										
										
										
											2021-06-30 14:27:18 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     async force(): Promise<void> { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |       return checkForUpdatesMaybeInstall(getMainWindow, logger, true); | 
					
						
							| 
									
										
										
										
											2021-06-30 14:27:18 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  | async function checkForUpdatesMaybeInstall( | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |   getMainWindow: () => BrowserWindow | undefined, | 
					
						
							| 
									
										
										
										
											2021-06-30 14:27:18 -07:00
										 |  |  |   logger: LoggerType, | 
					
						
							|  |  |  |   force = false | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | ) { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |   logger.info('checkForUpdatesMaybeInstall: checking for update...'); | 
					
						
							|  |  |  |   const result = await checkForUpdates(logger, force); | 
					
						
							|  |  |  |   if (!result) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const { fileName: newFileName, version: newVersion } = result; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (fileName !== newFileName || !version || gt(newVersion, version)) { | 
					
						
							|  |  |  |     const autoDownloadUpdates = await getAutoDownloadUpdateSetting( | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |       getMainWindow(), | 
					
						
							|  |  |  |       logger | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |     ); | 
					
						
							|  |  |  |     if (!autoDownloadUpdates) { | 
					
						
							| 
									
										
										
										
											2021-08-23 18:45:11 -04:00
										 |  |  |       setUpdateListener(async () => { | 
					
						
							|  |  |  |         logger.info( | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |           'checkForUpdatesMaybeInstall: have not downloaded update, going to download' | 
					
						
							| 
									
										
										
										
											2021-08-23 18:45:11 -04:00
										 |  |  |         ); | 
					
						
							|  |  |  |         await downloadAndInstall( | 
					
						
							|  |  |  |           newFileName, | 
					
						
							|  |  |  |           newVersion, | 
					
						
							|  |  |  |           getMainWindow, | 
					
						
							|  |  |  |           logger, | 
					
						
							|  |  |  |           true | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |       const mainWindow = getMainWindow(); | 
					
						
							|  |  |  |       if (mainWindow) { | 
					
						
							|  |  |  |         mainWindow.webContents.send( | 
					
						
							|  |  |  |           'show-update-dialog', | 
					
						
							|  |  |  |           DialogType.DownloadReady, | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             downloadSize: result.size, | 
					
						
							|  |  |  |             version: result.version, | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         logger.warn( | 
					
						
							|  |  |  |           'checkForUpdatesMaybeInstall: No mainWindow, not showing update dialog' | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |     await downloadAndInstall(newFileName, newVersion, getMainWindow, logger); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  | async function downloadAndInstall( | 
					
						
							|  |  |  |   newFileName: string, | 
					
						
							|  |  |  |   newVersion: string, | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |   getMainWindow: () => BrowserWindow | undefined, | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |   logger: LoggerType, | 
					
						
							|  |  |  |   updateOnProgress?: boolean | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     const oldFileName = fileName; | 
					
						
							|  |  |  |     const oldVersion = version; | 
					
						
							| 
									
										
										
										
											2021-07-15 17:57:34 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |     deleteCache(updateFilePath, logger); | 
					
						
							|  |  |  |     fileName = newFileName; | 
					
						
							|  |  |  |     version = newVersion; | 
					
						
							| 
									
										
										
										
											2021-07-15 17:57:34 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |     try { | 
					
						
							|  |  |  |       updateFilePath = await downloadUpdate( | 
					
						
							|  |  |  |         fileName, | 
					
						
							|  |  |  |         logger, | 
					
						
							|  |  |  |         updateOnProgress ? getMainWindow() : undefined | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } catch (error) { | 
					
						
							|  |  |  |       // Restore state in case of download error
 | 
					
						
							|  |  |  |       fileName = oldFileName; | 
					
						
							|  |  |  |       version = oldVersion; | 
					
						
							|  |  |  |       throw error; | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-27 13:21:42 -07:00
										 |  |  |     const publicKey = hexToBinary(config.get('updatesPublicKey')); | 
					
						
							| 
									
										
										
										
											2019-08-02 14:11:10 -07:00
										 |  |  |     const verified = await verifySignature(updateFilePath, version, publicKey); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |     if (!verified) { | 
					
						
							|  |  |  |       // Note: We don't delete the cache here, because we don't want to continually
 | 
					
						
							|  |  |  |       //   re-download the broken release. We will download it only once per launch.
 | 
					
						
							|  |  |  |       throw new Error( | 
					
						
							|  |  |  |         `Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')` | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |     logger.info('downloadAndInstall: showing dialog...'); | 
					
						
							| 
									
										
										
										
											2021-08-23 18:45:11 -04:00
										 |  |  |     setUpdateListener(async () => { | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         await verifyAndInstall(updateFilePath, newVersion, logger); | 
					
						
							|  |  |  |         installing = true; | 
					
						
							|  |  |  |       } catch (error) { | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  |         const mainWindow = getMainWindow(); | 
					
						
							|  |  |  |         if (mainWindow) { | 
					
						
							|  |  |  |           logger.info( | 
					
						
							|  |  |  |             'createUpdater: showing general update failure dialog...' | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |           mainWindow.webContents.send( | 
					
						
							|  |  |  |             'show-update-dialog', | 
					
						
							|  |  |  |             DialogType.Cannot_Update | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           logger.warn('createUpdater: no mainWindow, just failing over...'); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-08-23 18:45:11 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         throw error; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       markShouldQuit(); | 
					
						
							|  |  |  |       app.quit(); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2021-10-01 11:49:59 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     const mainWindow = getMainWindow(); | 
					
						
							|  |  |  |     if (mainWindow) { | 
					
						
							|  |  |  |       mainWindow.webContents.send('show-update-dialog', DialogType.Update, { | 
					
						
							|  |  |  |         version, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       logger.warn( | 
					
						
							|  |  |  |         'downloadAndInstall: no mainWindow, cannot show update dialog' | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   } catch (error) { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |     logger.error(`downloadAndInstall: ${getPrintableError(error)}`); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function quitHandler() { | 
					
						
							|  |  |  |   if (updateFilePath && !installing) { | 
					
						
							|  |  |  |     verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch( | 
					
						
							|  |  |  |       error => { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |         loggerForQuitHandler.error(`quitHandler: ${getPrintableError(error)}`); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Helpers
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // This is fixed by out new install mechanisms...
 | 
					
						
							|  |  |  | //   https://github.com/signalapp/Signal-Desktop/issues/2369
 | 
					
						
							|  |  |  | // ...but we should also clean up those old installers.
 | 
					
						
							|  |  |  | const IS_EXE = /\.exe$/i; | 
					
						
							|  |  |  | async function deletePreviousInstallers(logger: LoggerType) { | 
					
						
							|  |  |  |   const userDataPath = app.getPath('userData'); | 
					
						
							|  |  |  |   const files: Array<string> = await readdir(userDataPath); | 
					
						
							|  |  |  |   await Promise.all( | 
					
						
							|  |  |  |     files.map(async file => { | 
					
						
							|  |  |  |       const isExe = IS_EXE.test(file); | 
					
						
							|  |  |  |       if (!isExe) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const fullPath = join(userDataPath, file); | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         await unlink(fullPath); | 
					
						
							|  |  |  |       } catch (error) { | 
					
						
							|  |  |  |         logger.error(`deletePreviousInstallers: couldn't delete file ${file}`); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function verifyAndInstall( | 
					
						
							|  |  |  |   filePath: string, | 
					
						
							|  |  |  |   newVersion: string, | 
					
						
							|  |  |  |   logger: LoggerType | 
					
						
							|  |  |  | ) { | 
					
						
							| 
									
										
										
										
											2020-07-20 14:42:26 -04:00
										 |  |  |   if (installing) { | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-27 13:21:42 -07:00
										 |  |  |   const publicKey = hexToBinary(config.get('updatesPublicKey')); | 
					
						
							| 
									
										
										
										
											2019-08-02 14:11:10 -07:00
										 |  |  |   const verified = await verifySignature(updateFilePath, newVersion, publicKey); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   if (!verified) { | 
					
						
							|  |  |  |     throw new Error( | 
					
						
							|  |  |  |       `Downloaded update did not pass signature verification (version: '${newVersion}'; fileName: '${fileName}')` | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   await install(filePath, logger); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function install(filePath: string, logger: LoggerType): Promise<void> { | 
					
						
							|  |  |  |   logger.info('windows/install: installing package...'); | 
					
						
							|  |  |  |   const args = ['--updated']; | 
					
						
							|  |  |  |   const options = { | 
					
						
							|  |  |  |     detached: true, | 
					
						
							| 
									
										
										
										
											2020-09-16 12:31:05 -07:00
										 |  |  |     stdio: 'ignore' as const, // TypeScript considers this a plain string without help
 | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   try { | 
					
						
							|  |  |  |     await spawn(filePath, args, options); | 
					
						
							|  |  |  |   } catch (error) { | 
					
						
							|  |  |  |     if (error.code === 'UNKNOWN' || error.code === 'EACCES') { | 
					
						
							|  |  |  |       logger.warn( | 
					
						
							|  |  |  |         'windows/install: Error running installer; Trying again with elevate.exe' | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       await spawn(getElevatePath(), [filePath, ...args], options); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     throw error; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function deleteCache(filePath: string | null, logger: LoggerType) { | 
					
						
							|  |  |  |   if (filePath) { | 
					
						
							|  |  |  |     const tempDir = dirname(filePath); | 
					
						
							|  |  |  |     deleteTempDir(tempDir).catch(error => { | 
					
						
							| 
									
										
										
										
											2021-08-19 18:56:29 -04:00
										 |  |  |       logger.error(`deleteCache: ${getPrintableError(error)}`); | 
					
						
							| 
									
										
										
										
											2019-03-28 10:09:26 -07:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | function getElevatePath() { | 
					
						
							|  |  |  |   const installPath = app.getAppPath(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return join(installPath, 'resources', 'elevate.exe'); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function spawn( | 
					
						
							|  |  |  |   exe: string, | 
					
						
							|  |  |  |   args: Array<string>, | 
					
						
							|  |  |  |   options: SpawnOptions | 
					
						
							|  |  |  | ): Promise<void> { | 
					
						
							|  |  |  |   return new Promise((resolve, reject) => { | 
					
						
							|  |  |  |     const emitter = spawnEmitter(exe, args, options); | 
					
						
							|  |  |  |     emitter.on('error', reject); | 
					
						
							|  |  |  |     emitter.unref(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setTimeout(resolve, 200); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | } |