Use invoke/handle in settingsChannel

This commit is contained in:
Fedor Indutny 2021-11-10 01:56:56 +01:00 committed by GitHub
parent 37992715cd
commit 631e36dc0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 597 additions and 810 deletions

View file

@ -5,169 +5,31 @@ import { createReadStream, statSync } from 'fs';
import type { IncomingMessage, Server, ServerResponse } from 'http';
import { createServer } from 'http';
import type { AddressInfo } from 'net';
import { dirname } from 'path';
import { v4 as getGuid } from 'uuid';
import type { BrowserWindow } from 'electron';
import { app, autoUpdater } from 'electron';
import config from 'config';
import { gt } from 'semver';
import { autoUpdater } from 'electron';
import got from 'got';
import type { UpdaterInterface } from './common';
import {
checkForUpdates,
deleteTempDir,
downloadUpdate,
getAutoDownloadUpdateSetting,
getPrintableError,
setUpdateListener,
} from './common';
import * as durations from '../util/durations';
import type { LoggerType } from '../types/Logging';
import { hexToBinary, verifySignature } from './signature';
import { Updater } from './common';
import { explodePromise } from '../util/explodePromise';
import * as Errors from '../types/errors';
import { markShouldQuit } from '../../app/window_state';
import { DialogType } from '../types/Dialogs';
const INTERVAL = 30 * durations.MINUTE;
export async function start(
getMainWindow: () => BrowserWindow | undefined,
logger: LoggerType
): Promise<UpdaterInterface> {
logger.info('macos/start: starting checks...');
loggerForQuitHandler = logger;
app.once('quit', quitHandler);
setInterval(async () => {
try {
await checkForUpdatesMaybeInstall(getMainWindow, logger);
} catch (error) {
logger.error(`macos/start: ${getPrintableError(error)}`);
}
}, INTERVAL);
await checkForUpdatesMaybeInstall(getMainWindow, logger);
return {
async force(): Promise<void> {
return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
},
};
}
let fileName: string;
let version: string;
let updateFilePath: string;
let loggerForQuitHandler: LoggerType;
async function checkForUpdatesMaybeInstall(
getMainWindow: () => BrowserWindow | undefined,
logger: LoggerType,
force = false
) {
logger.info('checkForUpdatesMaybeInstall: checking for update...');
const result = await checkForUpdates(logger, force);
if (!result) {
return;
export class MacOSUpdater extends Updater {
protected async deletePreviousInstallers(): Promise<void> {
// No installers are cache on macOS
}
const { fileName: newFileName, version: newVersion } = result;
if (
force ||
fileName !== newFileName ||
!version ||
gt(newVersion, version)
) {
const autoDownloadUpdates = await getAutoDownloadUpdateSetting(
getMainWindow(),
logger
);
if (!autoDownloadUpdates) {
setUpdateListener(async () => {
logger.info(
'checkForUpdatesMaybeInstall: have not downloaded update, going to download'
);
await downloadAndInstall(
newFileName,
newVersion,
getMainWindow,
logger,
true
);
});
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, cannot show update dialog'
);
}
return;
}
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
}
}
async function downloadAndInstall(
newFileName: string,
newVersion: string,
getMainWindow: () => BrowserWindow | undefined,
logger: LoggerType,
updateOnProgress?: boolean
) {
try {
const oldFileName = fileName;
const oldVersion = version;
deleteCache(updateFilePath, logger);
fileName = newFileName;
version = newVersion;
try {
updateFilePath = await downloadUpdate(
fileName,
logger,
updateOnProgress ? getMainWindow() : undefined
);
} catch (error) {
// Restore state in case of download error
fileName = oldFileName;
version = oldVersion;
throw error;
}
if (!updateFilePath) {
logger.info('downloadAndInstall: no update file path. Skipping!');
return;
}
const publicKey = hexToBinary(config.get('updatesPublicKey'));
const verified = await 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(
`downloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
);
}
protected async installUpdate(updateFilePath: string): Promise<void> {
const { logger } = this;
try {
await handToAutoUpdate(updateFilePath, logger);
await this.handToAutoUpdate(updateFilePath);
} catch (error) {
const readOnly = 'Cannot update while running on a read-only volume';
const message: string = error.message || '';
const mainWindow = getMainWindow();
const mainWindow = this.getMainWindow();
if (mainWindow && message.includes(readOnly)) {
logger.info('downloadAndInstall: showing read-only dialog...');
mainWindow.webContents.send(
@ -195,55 +57,25 @@ async function downloadAndInstall(
// because Squirrel has cached the update file and will do the right thing.
logger.info('downloadAndInstall: showing update dialog...');
setUpdateListener(() => {
this.setUpdateListener(() => {
logger.info('performUpdate: calling quitAndInstall...');
markShouldQuit();
autoUpdater.quitAndInstall();
});
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.send('show-update-dialog', DialogType.Update, {
version,
});
} else {
logger.warn(
'checkForUpdatesMaybeInstall: no mainWindow, cannot show update dialog'
);
}
} catch (error) {
logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
}
}
function quitHandler() {
deleteCache(updateFilePath, loggerForQuitHandler);
}
private async handToAutoUpdate(filePath: string): Promise<void> {
const { logger } = this;
const { promise, resolve, reject } = explodePromise<void>();
// Helpers
function deleteCache(filePath: string | null, logger: LoggerType) {
if (filePath) {
const tempDir = dirname(filePath);
deleteTempDir(tempDir).catch(error => {
logger.error(`quitHandler: ${getPrintableError(error)}`);
});
}
}
async function handToAutoUpdate(
filePath: string,
logger: LoggerType
): Promise<void> {
return new Promise((resolve, reject) => {
const token = getGuid();
const updateFileUrl = generateFileUrl();
const server = createServer();
let serverUrl: string;
server.on('error', (error: Error) => {
logger.error(`handToAutoUpdate: ${getPrintableError(error)}`);
shutdown(server, logger);
logger.error(`handToAutoUpdate: ${Errors.toLogFormat(error)}`);
this.shutdown(server);
reject(error);
});
@ -266,12 +98,15 @@ async function handToAutoUpdate(
}
if (!url || !url.startsWith(updateFileUrl)) {
write404(url, response, logger);
this.logger.error(
`write404: Squirrel requested unexpected url '${url}'`
);
response.writeHead(404);
response.end();
return;
}
pipeUpdateToSquirrel(filePath, server, response, logger, reject);
this.pipeUpdateToSquirrel(filePath, server, response, reject);
}
);
@ -280,14 +115,14 @@ async function handToAutoUpdate(
serverUrl = getServerUrl(server);
autoUpdater.on('error', (...args) => {
logger.error('autoUpdater: error', ...args.map(getPrintableError));
logger.error('autoUpdater: error', ...args.map(Errors.toLogFormat));
const [error] = args;
reject(error);
});
autoUpdater.on('update-downloaded', () => {
logger.info('autoUpdater: update-downloaded event fired');
shutdown(server, logger);
this.shutdown(server);
resolve();
});
@ -307,46 +142,75 @@ async function handToAutoUpdate(
reject(error);
}
});
});
return promise;
}
private pipeUpdateToSquirrel(
filePath: string,
server: Server,
response: ServerResponse,
reject: (error: Error) => void
): void {
const { logger } = this;
const updateFileSize = getFileSize(filePath);
const readStream = createReadStream(filePath);
response.on('error', (error: Error) => {
logger.error(
`pipeUpdateToSquirrel: update file download request had an error ${Errors.toLogFormat(
error
)}`
);
this.shutdown(server);
reject(error);
});
readStream.on('error', (error: Error) => {
logger.error(
`pipeUpdateToSquirrel: read stream error response: ${Errors.toLogFormat(
error
)}`
);
this.shutdown(server, response);
reject(error);
});
response.writeHead(200, {
'Content-Type': 'application/zip',
'Content-Length': updateFileSize,
});
readStream.pipe(response);
}
private shutdown(server: Server, response?: ServerResponse): void {
const { logger } = this;
try {
if (server) {
server.close();
}
} catch (error) {
logger.error(
`shutdown: Error closing server ${Errors.toLogFormat(error)}`
);
}
try {
if (response) {
response.end();
}
} catch (endError) {
logger.error(
`shutdown: couldn't end response ${Errors.toLogFormat(endError)}`
);
}
}
}
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);
}
// Helpers
function writeJSONResponse(url: string, response: ServerResponse) {
const data = Buffer.from(
@ -374,16 +238,6 @@ function writeTokenResponse(token: string, response: ServerResponse) {
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;
@ -398,27 +252,3 @@ function getFileSize(targetPath: string): number {
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)}`
);
}
}