You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			232 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			232 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
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';
 | 
						|
import { get as getFromConfig } from 'config';
 | 
						|
import { gt } from 'semver';
 | 
						|
import pify from 'pify';
 | 
						|
 | 
						|
import {
 | 
						|
  checkForUpdates,
 | 
						|
  deleteTempDir,
 | 
						|
  downloadUpdate,
 | 
						|
  getPrintableError,
 | 
						|
  LoggerType,
 | 
						|
  MessagesType,
 | 
						|
  showCannotUpdateDialog,
 | 
						|
  showUpdateDialog,
 | 
						|
} from './common';
 | 
						|
import { hexToBinary, verifySignature } from './signature';
 | 
						|
import { markShouldQuit } from '../../app/window_state';
 | 
						|
 | 
						|
const readdir = pify(readdirCallback);
 | 
						|
const unlink = pify(unlinkCallback);
 | 
						|
 | 
						|
let isChecking = false;
 | 
						|
const SECOND = 1000;
 | 
						|
const MINUTE = SECOND * 60;
 | 
						|
const INTERVAL = MINUTE * 30;
 | 
						|
 | 
						|
export async function start(
 | 
						|
  getMainWindow: () => BrowserWindow,
 | 
						|
  messages: MessagesType,
 | 
						|
  logger: LoggerType
 | 
						|
) {
 | 
						|
  logger.info('windows/start: starting checks...');
 | 
						|
 | 
						|
  loggerForQuitHandler = logger;
 | 
						|
  app.once('quit', quitHandler);
 | 
						|
 | 
						|
  setInterval(async () => {
 | 
						|
    try {
 | 
						|
      await checkDownloadAndInstall(getMainWindow, messages, logger);
 | 
						|
    } catch (error) {
 | 
						|
      logger.error('windows/start: error:', getPrintableError(error));
 | 
						|
    }
 | 
						|
  }, INTERVAL);
 | 
						|
 | 
						|
  await deletePreviousInstallers(logger);
 | 
						|
  await checkDownloadAndInstall(getMainWindow, messages, logger);
 | 
						|
}
 | 
						|
 | 
						|
let fileName: string;
 | 
						|
let version: string;
 | 
						|
let updateFilePath: string;
 | 
						|
let installing: boolean;
 | 
						|
let loggerForQuitHandler: LoggerType;
 | 
						|
 | 
						|
async function checkDownloadAndInstall(
 | 
						|
  getMainWindow: () => BrowserWindow,
 | 
						|
  messages: MessagesType,
 | 
						|
  logger: LoggerType
 | 
						|
) {
 | 
						|
  if (isChecking) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  try {
 | 
						|
    isChecking = true;
 | 
						|
 | 
						|
    logger.info('checkDownloadAndInstall: checking for update...');
 | 
						|
    const result = await checkForUpdates(logger);
 | 
						|
    if (!result) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const { fileName: newFileName, version: newVersion } = result;
 | 
						|
    if (fileName !== newFileName || !version || gt(newVersion, version)) {
 | 
						|
      deleteCache(updateFilePath, logger);
 | 
						|
      fileName = newFileName;
 | 
						|
      version = newVersion;
 | 
						|
      updateFilePath = await downloadUpdate(fileName, logger);
 | 
						|
    }
 | 
						|
 | 
						|
    const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
 | 
						|
    const verified = verifySignature(updateFilePath, version, publicKey);
 | 
						|
    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}')`
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    logger.info('checkDownloadAndInstall: showing dialog...');
 | 
						|
    const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
 | 
						|
    if (!shouldUpdate) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      await verifyAndInstall(updateFilePath, version, logger);
 | 
						|
      installing = true;
 | 
						|
    } catch (error) {
 | 
						|
      logger.info(
 | 
						|
        'checkDownloadAndInstall: showing general update failure dialog...'
 | 
						|
      );
 | 
						|
      await showCannotUpdateDialog(getMainWindow(), messages);
 | 
						|
 | 
						|
      throw error;
 | 
						|
    }
 | 
						|
 | 
						|
    markShouldQuit();
 | 
						|
    app.quit();
 | 
						|
  } catch (error) {
 | 
						|
    logger.error('checkDownloadAndInstall: error', getPrintableError(error));
 | 
						|
  } finally {
 | 
						|
    isChecking = false;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function quitHandler() {
 | 
						|
  if (updateFilePath && !installing) {
 | 
						|
    verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
 | 
						|
      error => {
 | 
						|
        loggerForQuitHandler.error(
 | 
						|
          'quitHandler: error installing:',
 | 
						|
          getPrintableError(error)
 | 
						|
        );
 | 
						|
      }
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// 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
 | 
						|
) {
 | 
						|
  const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
 | 
						|
  const verified = verifySignature(updateFilePath, newVersion, publicKey);
 | 
						|
  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,
 | 
						|
    stdio: 'ignore' as 'ignore', // TypeScript considers this a plain string without help
 | 
						|
  };
 | 
						|
 | 
						|
  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 => {
 | 
						|
      logger.error(
 | 
						|
        'deleteCache: error deleting temporary directory',
 | 
						|
        getPrintableError(error)
 | 
						|
      );
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
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();
 | 
						|
 | 
						|
    // tslint:disable-next-line no-string-based-set-timeout
 | 
						|
    setTimeout(resolve, 200);
 | 
						|
  });
 | 
						|
}
 |