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.
		
		
		
		
		
			
		
			
				
	
	
		
			325 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			325 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			TypeScript
		
	
import { createReadStream, statSync } from 'fs';
 | 
						|
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
 | 
						|
import { AddressInfo } from 'net';
 | 
						|
import { dirname } from 'path';
 | 
						|
 | 
						|
import { v4 as getGuid } from 'uuid';
 | 
						|
import { app, autoUpdater, BrowserWindow, dialog } from 'electron';
 | 
						|
import { get as getFromConfig } from 'config';
 | 
						|
import { gt } from 'semver';
 | 
						|
 | 
						|
import {
 | 
						|
  checkForUpdates,
 | 
						|
  deleteTempDir,
 | 
						|
  downloadUpdate,
 | 
						|
  getPrintableError,
 | 
						|
  LoggerType,
 | 
						|
  MessagesType,
 | 
						|
  showCannotUpdateDialog,
 | 
						|
  showUpdateDialog,
 | 
						|
} from './common';
 | 
						|
import { hexToBinary, verifySignature } from './signature';
 | 
						|
import { markShouldQuit } from '../../app/window_state';
 | 
						|
 | 
						|
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('macos/start: starting checks...');
 | 
						|
 | 
						|
  loggerForQuitHandler = logger;
 | 
						|
  app.once('quit', quitHandler);
 | 
						|
 | 
						|
  setInterval(async () => {
 | 
						|
    try {
 | 
						|
      await checkDownloadAndInstall(getMainWindow, messages, logger);
 | 
						|
    } catch (error) {
 | 
						|
      logger.error('macos/start: error:', getPrintableError(error));
 | 
						|
    }
 | 
						|
  }, INTERVAL);
 | 
						|
 | 
						|
  await checkDownloadAndInstall(getMainWindow, messages, logger);
 | 
						|
}
 | 
						|
 | 
						|
let fileName: string;
 | 
						|
let version: string;
 | 
						|
let updateFilePath: string;
 | 
						|
let loggerForQuitHandler: LoggerType;
 | 
						|
 | 
						|
async function checkDownloadAndInstall(
 | 
						|
  getMainWindow: () => BrowserWindow,
 | 
						|
  messages: MessagesType,
 | 
						|
  logger: LoggerType
 | 
						|
) {
 | 
						|
  if (isChecking) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  logger.info('checkDownloadAndInstall: checking for update...');
 | 
						|
  try {
 | 
						|
    isChecking = true;
 | 
						|
 | 
						|
    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(
 | 
						|
        `checkDownloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    try {
 | 
						|
      await handToAutoUpdate(updateFilePath, logger);
 | 
						|
    } catch (error) {
 | 
						|
      const readOnly = 'Cannot update while running on a read-only volume';
 | 
						|
      const message: string = error.message || '';
 | 
						|
      if (message.includes(readOnly)) {
 | 
						|
        logger.info('checkDownloadAndInstall: showing read-only dialog...');
 | 
						|
        await showReadOnlyDialog(getMainWindow(), messages);
 | 
						|
      } else {
 | 
						|
        logger.info(
 | 
						|
          'checkDownloadAndInstall: showing general update failure dialog...'
 | 
						|
        );
 | 
						|
        await showCannotUpdateDialog(getMainWindow(), messages);
 | 
						|
      }
 | 
						|
 | 
						|
      throw error;
 | 
						|
    }
 | 
						|
 | 
						|
    // At this point, closing the app will cause the update to be installed automatically
 | 
						|
    //   because Squirrel has cached the update file and will do the right thing.
 | 
						|
 | 
						|
    logger.info('checkDownloadAndInstall: showing update dialog...');
 | 
						|
    const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
 | 
						|
    if (!shouldUpdate) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    logger.info('checkDownloadAndInstall: calling quitAndInstall...');
 | 
						|
    markShouldQuit();
 | 
						|
    autoUpdater.quitAndInstall();
 | 
						|
  } catch (error) {
 | 
						|
    logger.error('checkDownloadAndInstall: error', getPrintableError(error));
 | 
						|
  } finally {
 | 
						|
    isChecking = false;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function quitHandler() {
 | 
						|
  deleteCache(updateFilePath, loggerForQuitHandler);
 | 
						|
}
 | 
						|
 | 
						|
// Helpers
 | 
						|
 | 
						|
function deleteCache(filePath: string | null, logger: LoggerType) {
 | 
						|
  if (filePath) {
 | 
						|
    const tempDir = dirname(filePath);
 | 
						|
    deleteTempDir(tempDir).catch(error => {
 | 
						|
      logger.error(
 | 
						|
        'quitHandler: error deleting temporary directory:',
 | 
						|
        getPrintableError(error)
 | 
						|
      );
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
async function handToAutoUpdate(
 | 
						|
  filePath: string,
 | 
						|
  logger: LoggerType
 | 
						|
): Promise<void> {
 | 
						|
  return new Promise((resolve, reject) => {
 | 
						|
    const updateFileUrl = generateFileUrl();
 | 
						|
    const server = createServer();
 | 
						|
    let serverUrl: string;
 | 
						|
 | 
						|
    server.on('error', (error: Error) => {
 | 
						|
      logger.error(
 | 
						|
        'handToAutoUpdate: server had error',
 | 
						|
        getPrintableError(error)
 | 
						|
      );
 | 
						|
      shutdown(server, logger);
 | 
						|
      reject(error);
 | 
						|
    });
 | 
						|
 | 
						|
    server.on(
 | 
						|
      'request',
 | 
						|
      (request: IncomingMessage, response: ServerResponse) => {
 | 
						|
        const { url } = request;
 | 
						|
 | 
						|
        if (url === '/') {
 | 
						|
          const absoluteUrl = `${serverUrl}${updateFileUrl}`;
 | 
						|
          writeJSONResponse(absoluteUrl, response);
 | 
						|
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!url || !url.startsWith(updateFileUrl)) {
 | 
						|
          write404(url, response, logger);
 | 
						|
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        pipeUpdateToSquirrel(filePath, server, response, logger, reject);
 | 
						|
      }
 | 
						|
    );
 | 
						|
 | 
						|
    server.listen(0, '127.0.0.1', () => {
 | 
						|
      serverUrl = getServerUrl(server);
 | 
						|
 | 
						|
      autoUpdater.on('error', (error: Error) => {
 | 
						|
        logger.error('autoUpdater: error', getPrintableError(error));
 | 
						|
        reject(error);
 | 
						|
      });
 | 
						|
      autoUpdater.on('update-downloaded', () => {
 | 
						|
        logger.info('autoUpdater: update-downloaded event fired');
 | 
						|
        shutdown(server, logger);
 | 
						|
        resolve();
 | 
						|
      });
 | 
						|
 | 
						|
      autoUpdater.setFeedURL({
 | 
						|
        url: serverUrl,
 | 
						|
        headers: { 'Cache-Control': 'no-cache' },
 | 
						|
      });
 | 
						|
      autoUpdater.checkForUpdates();
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function pipeUpdateToSquirrel(
 | 
						|
  filePath: string,
 | 
						|
  server: Server,
 | 
						|
  response: ServerResponse,
 | 
						|
  logger: LoggerType,
 | 
						|
  reject: (error: Error) => void
 | 
						|
) {
 | 
						|
  const updateFileSize = getFileSize(filePath);
 | 
						|
  const readStream = createReadStream(filePath);
 | 
						|
 | 
						|
  response.on('error', (error: Error) => {
 | 
						|
    logger.error(
 | 
						|
      'pipeUpdateToSquirrel: update file download request had an error',
 | 
						|
      getPrintableError(error)
 | 
						|
    );
 | 
						|
    shutdown(server, logger);
 | 
						|
    reject(error);
 | 
						|
  });
 | 
						|
 | 
						|
  readStream.on('error', (error: Error) => {
 | 
						|
    logger.error(
 | 
						|
      'pipeUpdateToSquirrel: read stream error response:',
 | 
						|
      getPrintableError(error)
 | 
						|
    );
 | 
						|
    shutdown(server, logger, response);
 | 
						|
    reject(error);
 | 
						|
  });
 | 
						|
 | 
						|
  response.writeHead(200, {
 | 
						|
    'Content-Type': 'application/zip',
 | 
						|
    'Content-Length': updateFileSize,
 | 
						|
  });
 | 
						|
 | 
						|
  readStream.pipe(response);
 | 
						|
}
 | 
						|
 | 
						|
function writeJSONResponse(url: string, response: ServerResponse) {
 | 
						|
  const data = Buffer.from(
 | 
						|
    JSON.stringify({
 | 
						|
      url,
 | 
						|
    })
 | 
						|
  );
 | 
						|
  response.writeHead(200, {
 | 
						|
    'Content-Type': 'application/json',
 | 
						|
    'Content-Length': data.byteLength,
 | 
						|
  });
 | 
						|
  response.end(data);
 | 
						|
}
 | 
						|
 | 
						|
function write404(
 | 
						|
  url: string | undefined,
 | 
						|
  response: ServerResponse,
 | 
						|
  logger: LoggerType
 | 
						|
) {
 | 
						|
  logger.error(`write404: Squirrel requested unexpected url '${url}'`);
 | 
						|
  response.writeHead(404);
 | 
						|
  response.end();
 | 
						|
}
 | 
						|
 | 
						|
function getServerUrl(server: Server) {
 | 
						|
  const address = server.address() as AddressInfo;
 | 
						|
 | 
						|
  // tslint:disable-next-line:no-http-string
 | 
						|
  return `http://127.0.0.1:${address.port}`;
 | 
						|
}
 | 
						|
function generateFileUrl(): string {
 | 
						|
  return `/${getGuid()}.zip`;
 | 
						|
}
 | 
						|
 | 
						|
function getFileSize(targetPath: string): number {
 | 
						|
  const { size } = statSync(targetPath);
 | 
						|
 | 
						|
  return size;
 | 
						|
}
 | 
						|
 | 
						|
function shutdown(
 | 
						|
  server: Server,
 | 
						|
  logger: LoggerType,
 | 
						|
  response?: ServerResponse
 | 
						|
) {
 | 
						|
  try {
 | 
						|
    if (server) {
 | 
						|
      server.close();
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    logger.error('shutdown: Error closing server', getPrintableError(error));
 | 
						|
  }
 | 
						|
 | 
						|
  try {
 | 
						|
    if (response) {
 | 
						|
      response.end();
 | 
						|
    }
 | 
						|
  } catch (endError) {
 | 
						|
    logger.error(
 | 
						|
      "shutdown: couldn't end response",
 | 
						|
      getPrintableError(endError)
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export async function showReadOnlyDialog(
 | 
						|
  mainWindow: BrowserWindow,
 | 
						|
  messages: MessagesType
 | 
						|
): Promise<void> {
 | 
						|
  const options = {
 | 
						|
    type: 'warning',
 | 
						|
    buttons: [messages.ok.message],
 | 
						|
    title: messages.cannotUpdate.message,
 | 
						|
    message: messages.readOnlyVolume.message,
 | 
						|
  };
 | 
						|
 | 
						|
  return new Promise(resolve => {
 | 
						|
    dialog.showMessageBox(mainWindow, options, () => {
 | 
						|
      resolve();
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 |