New option for control over update downloads
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from 'fs';
|
||||
import { join, normalize } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { createParser, ParserConfiguration } from 'dashdash';
|
||||
import ProxyAgent from 'proxy-agent';
|
||||
@@ -20,10 +21,10 @@ import { v4 as getGuid } from 'uuid';
|
||||
import pify from 'pify';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import { getTempPath } from '../../app/attachments';
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
import { isAlpha, isBeta } from '../util/version';
|
||||
|
||||
@@ -31,7 +32,6 @@ import * as packageJson from '../../package.json';
|
||||
import { getSignatureFileName } from './signature';
|
||||
import { isPathInside } from '../util/isPathInside';
|
||||
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
|
||||
const writeFile = pify(writeFileCallback);
|
||||
@@ -39,24 +39,40 @@ const mkdirpPromise = pify(mkdirp);
|
||||
const rimrafPromise = pify(rimraf);
|
||||
const { platform } = process;
|
||||
|
||||
export const ACK_RENDER_TIMEOUT = 10000;
|
||||
export const GOT_CONNECT_TIMEOUT = 2 * 60 * 1000;
|
||||
export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000;
|
||||
export const GOT_SOCKET_TIMEOUT = 2 * 60 * 1000;
|
||||
|
||||
type JSONUpdateSchema = {
|
||||
version: string;
|
||||
files: Array<{
|
||||
url: string;
|
||||
sha512: string;
|
||||
size: string;
|
||||
blockMapSize?: string;
|
||||
}>;
|
||||
path: string;
|
||||
sha512: string;
|
||||
releaseDate: string;
|
||||
};
|
||||
|
||||
export type UpdaterInterface = {
|
||||
force(): Promise<void>;
|
||||
};
|
||||
|
||||
export type UpdateInformationType = {
|
||||
fileName: string;
|
||||
size: number;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export async function checkForUpdates(
|
||||
logger: LoggerType,
|
||||
forceUpdate = false
|
||||
): Promise<{
|
||||
fileName: string;
|
||||
version: string;
|
||||
} | null> {
|
||||
): Promise<UpdateInformationType | null> {
|
||||
const yaml = await getUpdateYaml();
|
||||
const version = getVersion(yaml);
|
||||
const parsedYaml = parseYaml(yaml);
|
||||
const version = getVersion(parsedYaml);
|
||||
|
||||
if (!version) {
|
||||
logger.warn('checkForUpdates: no version extracted from downloaded yaml');
|
||||
@@ -70,8 +86,11 @@ export async function checkForUpdates(
|
||||
`forceUpdate=${forceUpdate}`
|
||||
);
|
||||
|
||||
const fileName = getUpdateFileName(parsedYaml);
|
||||
|
||||
return {
|
||||
fileName: getUpdateFileName(yaml),
|
||||
fileName,
|
||||
size: getSize(parsedYaml, fileName),
|
||||
version,
|
||||
};
|
||||
}
|
||||
@@ -95,7 +114,8 @@ export function validatePath(basePath: string, targetPath: string): void {
|
||||
|
||||
export async function downloadUpdate(
|
||||
fileName: string,
|
||||
logger: LoggerType
|
||||
logger: LoggerType,
|
||||
mainWindow?: BrowserWindow
|
||||
): Promise<string> {
|
||||
const baseUrl = getUpdatesBase();
|
||||
const updateFileUrl = `${baseUrl}/${fileName}`;
|
||||
@@ -121,6 +141,23 @@ export async function downloadUpdate(
|
||||
const writeStream = createWriteStream(targetUpdatePath);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (mainWindow) {
|
||||
let downloadedSize = 0;
|
||||
|
||||
const throttledSend = throttle(() => {
|
||||
mainWindow.webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.Downloading,
|
||||
{ downloadedSize }
|
||||
);
|
||||
}, 500);
|
||||
|
||||
downloadStream.on('data', data => {
|
||||
downloadedSize += data.length;
|
||||
throttledSend();
|
||||
});
|
||||
}
|
||||
|
||||
downloadStream.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
@@ -144,106 +181,6 @@ export async function downloadUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
let showingUpdateDialog = false;
|
||||
|
||||
async function showFallbackUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): Promise<boolean> {
|
||||
if (showingUpdateDialog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RESTART_BUTTON = 0;
|
||||
const LATER_BUTTON = 1;
|
||||
const options = {
|
||||
type: 'info',
|
||||
buttons: [
|
||||
locale.messages.autoUpdateRestartButtonLabel.message,
|
||||
locale.messages.autoUpdateLaterButtonLabel.message,
|
||||
],
|
||||
title: locale.messages.autoUpdateNewVersionTitle.message,
|
||||
message: locale.messages.autoUpdateNewVersionMessage.message,
|
||||
detail: locale.messages.autoUpdateNewVersionInstructions.message,
|
||||
defaultId: LATER_BUTTON,
|
||||
cancelId: LATER_BUTTON,
|
||||
};
|
||||
|
||||
showingUpdateDialog = true;
|
||||
|
||||
const { response } = await dialog.showMessageBox(mainWindow, options);
|
||||
|
||||
showingUpdateDialog = false;
|
||||
|
||||
return response === RESTART_BUTTON;
|
||||
}
|
||||
|
||||
export function showUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType,
|
||||
performUpdateCallback: () => void
|
||||
): void {
|
||||
let ack = false;
|
||||
|
||||
ipcMain.once('show-update-dialog-ack', () => {
|
||||
ack = true;
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('show-update-dialog', Dialogs.Update);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (!ack) {
|
||||
const shouldUpdate = await showFallbackUpdateDialog(mainWindow, locale);
|
||||
if (shouldUpdate) {
|
||||
performUpdateCallback();
|
||||
}
|
||||
}
|
||||
}, ACK_RENDER_TIMEOUT);
|
||||
}
|
||||
|
||||
let showingCannotUpdateDialog = false;
|
||||
|
||||
async function showFallbackCannotUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): Promise<void> {
|
||||
if (showingCannotUpdateDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'error',
|
||||
buttons: [locale.messages.ok.message],
|
||||
title: locale.messages.cannotUpdate.message,
|
||||
message: locale.i18n('cannotUpdateDetail', ['https://signal.org/download']),
|
||||
};
|
||||
|
||||
showingCannotUpdateDialog = true;
|
||||
|
||||
await dialog.showMessageBox(mainWindow, options);
|
||||
|
||||
showingCannotUpdateDialog = false;
|
||||
}
|
||||
|
||||
export function showCannotUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): void {
|
||||
let ack = false;
|
||||
|
||||
ipcMain.once('show-update-dialog-ack', () => {
|
||||
ack = true;
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('show-update-dialog', Dialogs.Cannot_Update);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (!ack) {
|
||||
await showFallbackCannotUpdateDialog(mainWindow, locale);
|
||||
}
|
||||
}, ACK_RENDER_TIMEOUT);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
export function getUpdateCheckUrl(): string {
|
||||
@@ -288,9 +225,7 @@ function isVersionNewer(newVersion: string): boolean {
|
||||
return gt(newVersion, version);
|
||||
}
|
||||
|
||||
export function getVersion(yaml: string): string | null {
|
||||
const info = parseYaml(yaml);
|
||||
|
||||
export function getVersion(info: JSONUpdateSchema): string | null {
|
||||
return info && info.version;
|
||||
}
|
||||
|
||||
@@ -299,11 +234,7 @@ export function isUpdateFileNameValid(name: string): boolean {
|
||||
return validFile.test(name);
|
||||
}
|
||||
|
||||
// Reliant on third party parser that returns any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getUpdateFileName(yaml: string): any {
|
||||
const info = parseYaml(yaml);
|
||||
|
||||
export function getUpdateFileName(info: JSONUpdateSchema): string {
|
||||
if (!info || !info.path) {
|
||||
throw new Error('getUpdateFileName: No path present in YAML file');
|
||||
}
|
||||
@@ -318,9 +249,17 @@ export function getUpdateFileName(yaml: string): any {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Reliant on third party parser that returns any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function parseYaml(yaml: string): any {
|
||||
function getSize(info: JSONUpdateSchema, fileName: string): number {
|
||||
if (!info || !info.files) {
|
||||
throw new Error('getUpdateFileName: No files present in YAML file');
|
||||
}
|
||||
|
||||
const foundFile = info.files.find(file => file.url === fileName);
|
||||
|
||||
return Number(foundFile?.size) || 0;
|
||||
}
|
||||
|
||||
export function parseYaml(yaml: string): JSONUpdateSchema {
|
||||
return safeLoad(yaml, { schema: FAILSAFE_SCHEMA, json: true });
|
||||
}
|
||||
|
||||
@@ -413,3 +352,21 @@ export function getCliOptions<T>(options: ParserConfiguration['options']): T {
|
||||
export function setUpdateListener(performUpdateCallback: () => void): void {
|
||||
ipcMain.once('start-update', performUpdateCallback);
|
||||
}
|
||||
|
||||
export function getAutoDownloadUpdateSetting(
|
||||
mainWindow: BrowserWindow
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcMain.once(
|
||||
'settings:get-success:autoDownloadUpdate',
|
||||
(_, error, value: boolean) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(value);
|
||||
}
|
||||
}
|
||||
);
|
||||
mainWindow.webContents.send('settings:get:autoDownloadUpdate');
|
||||
});
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ import { BrowserWindow } from 'electron';
|
||||
import { UpdaterInterface } from './common';
|
||||
import { start as startMacOS } from './macos';
|
||||
import { start as startWindows } from './windows';
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
|
||||
let initialized = false;
|
||||
@@ -16,7 +15,6 @@ let updater: UpdaterInterface | undefined;
|
||||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale?: LocaleType,
|
||||
logger?: LoggerType
|
||||
): Promise<void> {
|
||||
const { platform } = process;
|
||||
@@ -26,9 +24,6 @@ export async function start(
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
if (!locale) {
|
||||
throw new Error('updater/start: Must provide locale!');
|
||||
}
|
||||
if (!logger) {
|
||||
throw new Error('updater/start: Must provide logger!');
|
||||
}
|
||||
@@ -42,9 +37,9 @@ export async function start(
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
updater = await startWindows(getMainWindow, locale, logger);
|
||||
updater = await startWindows(getMainWindow, logger);
|
||||
} else if (platform === 'darwin') {
|
||||
updater = await startMacOS(getMainWindow, locale, logger);
|
||||
updater = await startMacOS(getMainWindow, logger);
|
||||
} else {
|
||||
throw new Error('updater/start: Unsupported platform');
|
||||
}
|
||||
|
@@ -7,27 +7,25 @@ import { AddressInfo } from 'net';
|
||||
import { dirname } from 'path';
|
||||
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { app, autoUpdater, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||
import { app, autoUpdater, BrowserWindow } from 'electron';
|
||||
import { get as getFromConfig } from 'config';
|
||||
import { gt } from 'semver';
|
||||
import got from 'got';
|
||||
|
||||
import {
|
||||
ACK_RENDER_TIMEOUT,
|
||||
checkForUpdates,
|
||||
deleteTempDir,
|
||||
downloadUpdate,
|
||||
getAutoDownloadUpdateSetting,
|
||||
getPrintableError,
|
||||
setUpdateListener,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
UpdaterInterface,
|
||||
UpdateInformationType,
|
||||
} from './common';
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
import { hexToBinary, verifySignature } from './signature';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = SECOND * 60;
|
||||
@@ -35,7 +33,6 @@ const INTERVAL = MINUTE * 30;
|
||||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType
|
||||
): Promise<UpdaterInterface> {
|
||||
logger.info('macos/start: starting checks...');
|
||||
@@ -45,19 +42,17 @@ export async function start(
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
} catch (error) {
|
||||
logger.error('macos/start: error:', getPrintableError(error));
|
||||
logger.error(`macos/start: ${getPrintableError(error)}`);
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
setUpdateListener(createUpdater(logger));
|
||||
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
|
||||
return {
|
||||
async force(): Promise<void> {
|
||||
return checkDownloadAndInstall(getMainWindow, locale, logger, true);
|
||||
return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -67,39 +62,69 @@ let version: string;
|
||||
let updateFilePath: string;
|
||||
let loggerForQuitHandler: LoggerType;
|
||||
|
||||
async function checkDownloadAndInstall(
|
||||
async function checkForUpdatesMaybeInstall(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType,
|
||||
force = false
|
||||
) {
|
||||
logger.info('checkDownloadAndInstall: checking for update...');
|
||||
try {
|
||||
const result = await checkForUpdates(logger, force);
|
||||
if (!result) {
|
||||
logger.info('checkForUpdatesMaybeInstall: checking for update...');
|
||||
const result = await checkForUpdates(logger, force);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
|
||||
setUpdateListener(createUpdater(getMainWindow, result, logger));
|
||||
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
const autoDownloadUpdates = await getAutoDownloadUpdateSetting(
|
||||
getMainWindow()
|
||||
);
|
||||
if (!autoDownloadUpdates) {
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.DownloadReady,
|
||||
{
|
||||
downloadSize: result.size,
|
||||
version: result.version,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
|
||||
}
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
const oldFileName = fileName;
|
||||
const oldVersion = version;
|
||||
async function downloadAndInstall(
|
||||
newFileName: string,
|
||||
newVersion: string,
|
||||
getMainWindow: () => BrowserWindow,
|
||||
logger: LoggerType,
|
||||
updateOnProgress?: boolean
|
||||
) {
|
||||
try {
|
||||
const oldFileName = fileName;
|
||||
const oldVersion = version;
|
||||
|
||||
deleteCache(updateFilePath, logger);
|
||||
fileName = newFileName;
|
||||
version = newVersion;
|
||||
try {
|
||||
updateFilePath = await downloadUpdate(fileName, logger);
|
||||
} catch (error) {
|
||||
// Restore state in case of download error
|
||||
fileName = oldFileName;
|
||||
version = oldVersion;
|
||||
throw error;
|
||||
}
|
||||
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('checkDownloadAndInstall: no update file path. Skipping!');
|
||||
logger.info('downloadAndInstall: no update file path. Skipping!');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,7 +134,7 @@ async function checkDownloadAndInstall(
|
||||
// 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}')`
|
||||
`downloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,13 +144,19 @@ async function checkDownloadAndInstall(
|
||||
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...');
|
||||
showReadOnlyDialog(getMainWindow(), locale);
|
||||
logger.info('downloadAndInstall: showing read-only dialog...');
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.MacOS_Read_Only
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
||||
'downloadAndInstall: showing general update failure dialog...'
|
||||
);
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.Cannot_Update
|
||||
);
|
||||
showCannotUpdateDialog(getMainWindow(), locale);
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -133,12 +164,13 @@ async function checkDownloadAndInstall(
|
||||
|
||||
// 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('downloadAndInstall: showing update dialog...');
|
||||
|
||||
logger.info('checkDownloadAndInstall: showing update dialog...');
|
||||
|
||||
showUpdateDialog(getMainWindow(), locale, createUpdater(logger));
|
||||
getMainWindow().webContents.send('show-update-dialog', DialogType.Update, {
|
||||
version,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
||||
logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +184,7 @@ 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)
|
||||
);
|
||||
logger.error(`quitHandler: ${getPrintableError(error)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -171,10 +200,7 @@ async function handToAutoUpdate(
|
||||
let serverUrl: string;
|
||||
|
||||
server.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'handToAutoUpdate: server had error',
|
||||
getPrintableError(error)
|
||||
);
|
||||
logger.error(`handToAutoUpdate: ${getPrintableError(error)}`);
|
||||
shutdown(server, logger);
|
||||
reject(error);
|
||||
});
|
||||
@@ -254,8 +280,9 @@ function pipeUpdateToSquirrel(
|
||||
|
||||
response.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'pipeUpdateToSquirrel: update file download request had an error',
|
||||
getPrintableError(error)
|
||||
`pipeUpdateToSquirrel: update file download request had an error ${getPrintableError(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
shutdown(server, logger);
|
||||
reject(error);
|
||||
@@ -263,8 +290,9 @@ function pipeUpdateToSquirrel(
|
||||
|
||||
readStream.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'pipeUpdateToSquirrel: read stream error response:',
|
||||
getPrintableError(error)
|
||||
`pipeUpdateToSquirrel: read stream error response: ${getPrintableError(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
shutdown(server, logger, response);
|
||||
reject(error);
|
||||
@@ -339,7 +367,7 @@ function shutdown(
|
||||
server.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('shutdown: Error closing server', getPrintableError(error));
|
||||
logger.error(`shutdown: Error closing server ${getPrintableError(error)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -348,62 +376,32 @@ function shutdown(
|
||||
}
|
||||
} catch (endError) {
|
||||
logger.error(
|
||||
"shutdown: couldn't end response",
|
||||
getPrintableError(endError)
|
||||
`shutdown: couldn't end response ${getPrintableError(endError)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function showReadOnlyDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): void {
|
||||
let ack = false;
|
||||
|
||||
ipcMain.once('show-update-dialog-ack', () => {
|
||||
ack = true;
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('show-update-dialog', Dialogs.MacOS_Read_Only);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (!ack) {
|
||||
await showFallbackReadOnlyDialog(mainWindow, locale);
|
||||
}
|
||||
}, ACK_RENDER_TIMEOUT);
|
||||
}
|
||||
|
||||
let showingReadOnlyDialog = false;
|
||||
|
||||
async function showFallbackReadOnlyDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
function createUpdater(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
info: Pick<UpdateInformationType, 'fileName' | 'version'>,
|
||||
logger: LoggerType
|
||||
) {
|
||||
if (showingReadOnlyDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'warning',
|
||||
buttons: [locale.messages.ok.message],
|
||||
title: locale.messages.cannotUpdate.message,
|
||||
message: locale.i18n('readOnlyVolume', {
|
||||
app: 'Signal.app',
|
||||
folder: '/Applications',
|
||||
}),
|
||||
};
|
||||
|
||||
showingReadOnlyDialog = true;
|
||||
|
||||
await dialog.showMessageBox(mainWindow, options);
|
||||
|
||||
showingReadOnlyDialog = false;
|
||||
}
|
||||
|
||||
function createUpdater(logger: LoggerType) {
|
||||
return () => {
|
||||
logger.info('performUpdate: calling quitAndInstall...');
|
||||
markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
return async () => {
|
||||
if (updateFilePath) {
|
||||
logger.info('performUpdate: calling quitAndInstall...');
|
||||
markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
logger.info(
|
||||
'performUpdate: have not downloaded update, going to download'
|
||||
);
|
||||
await downloadAndInstall(
|
||||
info.fileName,
|
||||
info.version,
|
||||
getMainWindow,
|
||||
logger,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -14,16 +14,16 @@ import {
|
||||
checkForUpdates,
|
||||
deleteTempDir,
|
||||
downloadUpdate,
|
||||
getAutoDownloadUpdateSetting,
|
||||
getPrintableError,
|
||||
setUpdateListener,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
UpdaterInterface,
|
||||
UpdateInformationType,
|
||||
} from './common';
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
import { hexToBinary, verifySignature } from './signature';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
||||
const readdir = pify(readdirCallback);
|
||||
const unlink = pify(unlinkCallback);
|
||||
@@ -40,7 +40,6 @@ let loggerForQuitHandler: LoggerType;
|
||||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType
|
||||
): Promise<UpdaterInterface> {
|
||||
logger.info('windows/start: starting checks...');
|
||||
@@ -48,56 +47,84 @@ export async function start(
|
||||
loggerForQuitHandler = logger;
|
||||
app.once('quit', quitHandler);
|
||||
|
||||
setUpdateListener(createUpdater(getMainWindow, locale, logger));
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
} catch (error) {
|
||||
logger.error('windows/start: error:', getPrintableError(error));
|
||||
logger.error(`windows/start: ${getPrintableError(error)}`);
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
await deletePreviousInstallers(logger);
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
|
||||
return {
|
||||
async force(): Promise<void> {
|
||||
return checkDownloadAndInstall(getMainWindow, locale, logger, true);
|
||||
return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function checkDownloadAndInstall(
|
||||
async function checkForUpdatesMaybeInstall(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType,
|
||||
force = false
|
||||
) {
|
||||
try {
|
||||
logger.info('checkDownloadAndInstall: checking for update...');
|
||||
const result = await checkForUpdates(logger, force);
|
||||
if (!result) {
|
||||
logger.info('checkForUpdatesMaybeInstall: checking for update...');
|
||||
const result = await checkForUpdates(logger, force);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
|
||||
setUpdateListener(createUpdater(getMainWindow, result, logger));
|
||||
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
const autoDownloadUpdates = await getAutoDownloadUpdateSetting(
|
||||
getMainWindow()
|
||||
);
|
||||
if (!autoDownloadUpdates) {
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.DownloadReady,
|
||||
{
|
||||
downloadSize: result.size,
|
||||
version: result.version,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
|
||||
}
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
const oldFileName = fileName;
|
||||
const oldVersion = version;
|
||||
async function downloadAndInstall(
|
||||
newFileName: string,
|
||||
newVersion: string,
|
||||
getMainWindow: () => BrowserWindow,
|
||||
logger: LoggerType,
|
||||
updateOnProgress?: boolean
|
||||
) {
|
||||
try {
|
||||
const oldFileName = fileName;
|
||||
const oldVersion = version;
|
||||
|
||||
deleteCache(updateFilePath, logger);
|
||||
fileName = newFileName;
|
||||
version = newVersion;
|
||||
deleteCache(updateFilePath, logger);
|
||||
fileName = newFileName;
|
||||
version = newVersion;
|
||||
|
||||
try {
|
||||
updateFilePath = await downloadUpdate(fileName, logger);
|
||||
} catch (error) {
|
||||
// Restore state in case of download error
|
||||
fileName = oldFileName;
|
||||
version = oldVersion;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
updateFilePath = await downloadUpdate(
|
||||
fileName,
|
||||
logger,
|
||||
updateOnProgress ? getMainWindow() : undefined
|
||||
);
|
||||
} catch (error) {
|
||||
// Restore state in case of download error
|
||||
fileName = oldFileName;
|
||||
version = oldVersion;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||
@@ -110,14 +137,12 @@ async function checkDownloadAndInstall(
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('checkDownloadAndInstall: showing dialog...');
|
||||
showUpdateDialog(
|
||||
getMainWindow(),
|
||||
locale,
|
||||
createUpdater(getMainWindow, locale, logger)
|
||||
);
|
||||
logger.info('downloadAndInstall: showing dialog...');
|
||||
getMainWindow().webContents.send('show-update-dialog', DialogType.Update, {
|
||||
version,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
||||
logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,10 +150,7 @@ function quitHandler() {
|
||||
if (updateFilePath && !installing) {
|
||||
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
|
||||
error => {
|
||||
loggerForQuitHandler.error(
|
||||
'quitHandler: error installing:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
loggerForQuitHandler.error(`quitHandler: ${getPrintableError(error)}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -208,10 +230,7 @@ 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)
|
||||
);
|
||||
logger.error(`deleteCache: ${getPrintableError(error)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -237,23 +256,37 @@ async function spawn(
|
||||
|
||||
function createUpdater(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
info: Pick<UpdateInformationType, 'fileName' | 'version'>,
|
||||
logger: LoggerType
|
||||
) {
|
||||
return async () => {
|
||||
try {
|
||||
await verifyAndInstall(updateFilePath, version, logger);
|
||||
installing = true;
|
||||
} catch (error) {
|
||||
if (updateFilePath) {
|
||||
try {
|
||||
await verifyAndInstall(updateFilePath, version, logger);
|
||||
installing = true;
|
||||
} catch (error) {
|
||||
logger.info('createUpdater: showing general update failure dialog...');
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.Cannot_Update
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
markShouldQuit();
|
||||
app.quit();
|
||||
} else {
|
||||
logger.info(
|
||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
||||
'performUpdate: have not downloaded update, going to download'
|
||||
);
|
||||
await downloadAndInstall(
|
||||
info.fileName,
|
||||
info.version,
|
||||
getMainWindow,
|
||||
logger,
|
||||
true
|
||||
);
|
||||
showCannotUpdateDialog(getMainWindow(), locale);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
markShouldQuit();
|
||||
app.quit();
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user