Use invoke
/handle
in settingsChannel
This commit is contained in:
parent
37992715cd
commit
631e36dc0a
10 changed files with 597 additions and 810 deletions
13
app/main.ts
13
app/main.ts
|
@ -30,6 +30,7 @@ import packageJson from '../package.json';
|
||||||
import * as GlobalErrors from './global_errors';
|
import * as GlobalErrors from './global_errors';
|
||||||
import { setup as setupSpellChecker } from './spell_check';
|
import { setup as setupSpellChecker } from './spell_check';
|
||||||
import { redactAll, addSensitivePath } from '../ts/util/privacy';
|
import { redactAll, addSensitivePath } from '../ts/util/privacy';
|
||||||
|
import { strictAssert } from '../ts/util/assert';
|
||||||
import { consoleLogger } from '../ts/util/consoleLogger';
|
import { consoleLogger } from '../ts/util/consoleLogger';
|
||||||
|
|
||||||
import './startup_config';
|
import './startup_config';
|
||||||
|
@ -378,7 +379,9 @@ function handleCommonWindowEvents(window: BrowserWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.webContents.send('callbacks:call:persistZoomFactor', [zoomFactor]);
|
settingsChannel?.invokeCallbackInMainWindow('persistZoomFactor', [
|
||||||
|
zoomFactor,
|
||||||
|
]);
|
||||||
|
|
||||||
lastZoomFactor = zoomFactor;
|
lastZoomFactor = zoomFactor;
|
||||||
};
|
};
|
||||||
|
@ -794,7 +797,11 @@ async function readyForUpdates() {
|
||||||
|
|
||||||
// Second, start checking for app updates
|
// Second, start checking for app updates
|
||||||
try {
|
try {
|
||||||
await updater.start(getMainWindow, getLogger());
|
strictAssert(
|
||||||
|
settingsChannel !== undefined,
|
||||||
|
'SettingsChannel must be initialized'
|
||||||
|
);
|
||||||
|
await updater.start(settingsChannel, getLogger(), getMainWindow);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
getLogger().error(
|
getLogger().error(
|
||||||
'Error starting update checks:',
|
'Error starting update checks:',
|
||||||
|
@ -1302,7 +1309,7 @@ const onDatabaseError = async (error: string) => {
|
||||||
ready = false;
|
ready = false;
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('callbacks:call:closeDB', []);
|
settingsChannel?.invokeCallbackInMainWindow('closeDB', []);
|
||||||
mainWindow.close();
|
mainWindow.close();
|
||||||
}
|
}
|
||||||
mainWindow = undefined;
|
mainWindow = undefined;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { userConfig } from '../../app/user_config';
|
||||||
import { ephemeralConfig } from '../../app/ephemeral_config';
|
import { ephemeralConfig } from '../../app/ephemeral_config';
|
||||||
import { installPermissionsHandler } from '../../app/permissions';
|
import { installPermissionsHandler } from '../../app/permissions';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
import { explodePromise } from '../util/explodePromise';
|
||||||
import type {
|
import type {
|
||||||
IPCEventsValuesType,
|
IPCEventsValuesType,
|
||||||
IPCEventsCallbacksType,
|
IPCEventsCallbacksType,
|
||||||
|
@ -18,13 +19,26 @@ const EPHEMERAL_NAME_MAP = new Map([
|
||||||
['systemTraySetting', 'system-tray-setting'],
|
['systemTraySetting', 'system-tray-setting'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
type ResponseQueueEntry = Readonly<{
|
||||||
|
resolve(value: unknown): void;
|
||||||
|
reject(error: Error): void;
|
||||||
|
}>;
|
||||||
|
|
||||||
export class SettingsChannel {
|
export class SettingsChannel {
|
||||||
private mainWindow?: BrowserWindow;
|
private mainWindow?: BrowserWindow;
|
||||||
|
|
||||||
|
private readonly responseQueue = new Map<number, ResponseQueueEntry>();
|
||||||
|
|
||||||
|
private responseSeq = 0;
|
||||||
|
|
||||||
public setMainWindow(mainWindow: BrowserWindow | undefined): void {
|
public setMainWindow(mainWindow: BrowserWindow | undefined): void {
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMainWindow(): BrowserWindow | undefined {
|
||||||
|
return this.mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
public install(): void {
|
public install(): void {
|
||||||
this.installSetting('deviceName', { setter: false });
|
this.installSetting('deviceName', { setter: false });
|
||||||
|
|
||||||
|
@ -91,83 +105,106 @@ export class SettingsChannel {
|
||||||
|
|
||||||
// These ones are different because its single source of truth is userConfig,
|
// These ones are different because its single source of truth is userConfig,
|
||||||
// not IndexedDB
|
// not IndexedDB
|
||||||
ipc.on('settings:get:mediaPermissions', event => {
|
ipc.handle('settings:get:mediaPermissions', () => {
|
||||||
event.sender.send(
|
return userConfig.get('mediaPermissions') || false;
|
||||||
'settings:get-success:mediaPermissions',
|
|
||||||
null,
|
|
||||||
userConfig.get('mediaPermissions') || false
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
ipc.on('settings:get:mediaCameraPermissions', event => {
|
ipc.handle('settings:get:mediaCameraPermissions', () => {
|
||||||
event.sender.send(
|
return userConfig.get('mediaCameraPermissions') || false;
|
||||||
'settings:get-success:mediaCameraPermissions',
|
|
||||||
null,
|
|
||||||
userConfig.get('mediaCameraPermissions') || false
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
ipc.on('settings:set:mediaPermissions', (event, value) => {
|
ipc.handle('settings:set:mediaPermissions', (_event, value) => {
|
||||||
userConfig.set('mediaPermissions', value);
|
userConfig.set('mediaPermissions', value);
|
||||||
|
|
||||||
// We reinstall permissions handler to ensure that a revoked permission takes effect
|
// We reinstall permissions handler to ensure that a revoked permission takes effect
|
||||||
installPermissionsHandler({ session, userConfig });
|
installPermissionsHandler({ session, userConfig });
|
||||||
|
|
||||||
event.sender.send('settings:set-success:mediaPermissions', null, value);
|
|
||||||
});
|
});
|
||||||
ipc.on('settings:set:mediaCameraPermissions', (event, value) => {
|
ipc.handle('settings:set:mediaCameraPermissions', (_event, value) => {
|
||||||
userConfig.set('mediaCameraPermissions', value);
|
userConfig.set('mediaCameraPermissions', value);
|
||||||
|
|
||||||
// We reinstall permissions handler to ensure that a revoked permission takes effect
|
// We reinstall permissions handler to ensure that a revoked permission takes effect
|
||||||
installPermissionsHandler({ session, userConfig });
|
installPermissionsHandler({ session, userConfig });
|
||||||
|
|
||||||
event.sender.send(
|
|
||||||
'settings:set-success:mediaCameraPermissions',
|
|
||||||
null,
|
|
||||||
value
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.on('settings:response', (_event, seq, error, value) => {
|
||||||
|
const entry = this.responseQueue.get(seq);
|
||||||
|
this.responseQueue.delete(seq);
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resolve, reject } = entry;
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private waitForResponse<Value>(): { promise: Promise<Value>; seq: number } {
|
||||||
|
const seq = this.responseSeq;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
this.responseSeq = (this.responseSeq + 1) & 0x7fffffff;
|
||||||
|
|
||||||
|
const { promise, resolve, reject } = explodePromise<Value>();
|
||||||
|
|
||||||
|
this.responseQueue.set(seq, { resolve, reject });
|
||||||
|
|
||||||
|
return { seq, promise };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSettingFromMainWindow<Name extends keyof IPCEventsValuesType>(
|
public getSettingFromMainWindow<Name extends keyof IPCEventsValuesType>(
|
||||||
name: Name
|
name: Name
|
||||||
): Promise<IPCEventsValuesType[Name]> {
|
): Promise<IPCEventsValuesType[Name]> {
|
||||||
const { mainWindow } = this;
|
const { mainWindow } = this;
|
||||||
return new Promise((resolve, reject) => {
|
if (!mainWindow || !mainWindow.webContents) {
|
||||||
ipc.once(`settings:get-success:${name}`, (_event, error, value) => {
|
throw new Error('No main window');
|
||||||
if (error) {
|
}
|
||||||
reject(error);
|
|
||||||
} else {
|
const { seq, promise } = this.waitForResponse<IPCEventsValuesType[Name]>();
|
||||||
resolve(value);
|
|
||||||
}
|
mainWindow.webContents.send(`settings:get:${name}`, { seq });
|
||||||
});
|
|
||||||
if (!mainWindow || !mainWindow.webContents) {
|
return promise;
|
||||||
reject(new Error('No main window available'));
|
}
|
||||||
return;
|
|
||||||
}
|
public setSettingInMainWindow<Name extends keyof IPCEventsValuesType>(
|
||||||
mainWindow.webContents.send(`settings:get:${name}`);
|
name: Name,
|
||||||
});
|
value: IPCEventsValuesType[Name]
|
||||||
|
): Promise<void> {
|
||||||
|
const { mainWindow } = this;
|
||||||
|
if (!mainWindow || !mainWindow.webContents) {
|
||||||
|
throw new Error('No main window');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { seq, promise } = this.waitForResponse<void>();
|
||||||
|
|
||||||
|
mainWindow.webContents.send(`settings:set:${name}`, { seq, value });
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public invokeCallbackInMainWindow<Name extends keyof IPCEventsCallbacksType>(
|
||||||
|
name: Name,
|
||||||
|
args: ReadonlyArray<unknown>
|
||||||
|
): Promise<unknown> {
|
||||||
|
const { mainWindow } = this;
|
||||||
|
if (!mainWindow || !mainWindow.webContents) {
|
||||||
|
throw new Error('Main window not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { seq, promise } = this.waitForResponse<unknown>();
|
||||||
|
|
||||||
|
mainWindow.webContents.send(`settings:call:${name}`, { seq, args });
|
||||||
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private installCallback<Name extends keyof IPCEventsCallbacksType>(
|
private installCallback<Name extends keyof IPCEventsCallbacksType>(
|
||||||
name: Name
|
name: Name
|
||||||
): void {
|
): void {
|
||||||
ipc.on(`callbacks:call:${name}`, async (event, args) => {
|
ipc.handle(`settings:call:${name}`, async (_event, args) => {
|
||||||
const { mainWindow } = this;
|
return this.invokeCallbackInMainWindow(name, args);
|
||||||
const contents = event.sender;
|
|
||||||
if (!mainWindow || !mainWindow.webContents) {
|
|
||||||
return contents.send(
|
|
||||||
`callbacks:call-success:${name}`,
|
|
||||||
'Main window not found'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.webContents.send(`callbacks:call:${name}`, args);
|
|
||||||
ipc.once(`callbacks:call-success:${name}`, (_event, error, value) => {
|
|
||||||
if (contents.isDestroyed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.send(`callbacks:call-success:${name}`, error, value);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,24 +217,8 @@ export class SettingsChannel {
|
||||||
}: { getter?: boolean; setter?: boolean; isEphemeral?: boolean } = {}
|
}: { getter?: boolean; setter?: boolean; isEphemeral?: boolean } = {}
|
||||||
): void {
|
): void {
|
||||||
if (getter) {
|
if (getter) {
|
||||||
ipc.on(`settings:get:${name}`, async event => {
|
ipc.handle(`settings:get:${name}`, async () => {
|
||||||
const { mainWindow } = this;
|
return this.getSettingFromMainWindow(name);
|
||||||
if (mainWindow && mainWindow.webContents) {
|
|
||||||
let error: Error | undefined;
|
|
||||||
let value: unknown;
|
|
||||||
try {
|
|
||||||
value = await this.getSettingFromMainWindow(name);
|
|
||||||
} catch (caughtError) {
|
|
||||||
error = caughtError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contents = event.sender;
|
|
||||||
if (contents.isDestroyed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.send(`settings:get-success:${name}`, error, value);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +226,7 @@ export class SettingsChannel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ipc.on(`settings:set:${name}`, (event, value) => {
|
ipc.handle(`settings:set:${name}`, (_event, value) => {
|
||||||
if (isEphemeral) {
|
if (isEphemeral) {
|
||||||
const ephemeralName = EPHEMERAL_NAME_MAP.get(name);
|
const ephemeralName = EPHEMERAL_NAME_MAP.get(name);
|
||||||
strictAssert(
|
strictAssert(
|
||||||
|
@ -215,18 +236,7 @@ export class SettingsChannel {
|
||||||
ephemeralConfig.set(ephemeralName, value);
|
ephemeralConfig.set(ephemeralName, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mainWindow } = this;
|
return this.setSettingInMainWindow(name, value);
|
||||||
if (mainWindow && mainWindow.webContents) {
|
|
||||||
ipc.once(`settings:set-success:${name}`, (_event, error) => {
|
|
||||||
const contents = event.sender;
|
|
||||||
if (contents.isDestroyed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
contents.send(`settings:set-success:${name}`, error);
|
|
||||||
});
|
|
||||||
mainWindow.webContents.send(`settings:set:${name}`, value);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
statSync,
|
statSync,
|
||||||
writeFile as writeFileCallback,
|
writeFile as writeFileCallback,
|
||||||
} from 'fs';
|
} from 'fs';
|
||||||
import { join, normalize } from 'path';
|
import { join, normalize, dirname } from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
|
@ -26,14 +26,21 @@ import rimraf from 'rimraf';
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain } from 'electron';
|
||||||
|
|
||||||
|
import * as durations from '../util/durations';
|
||||||
import { getTempPath } from '../util/attachments';
|
import { getTempPath } from '../util/attachments';
|
||||||
import { DialogType } from '../types/Dialogs';
|
import { DialogType } from '../types/Dialogs';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
import { getUserAgent } from '../util/getUserAgent';
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
import { isAlpha, isBeta } from '../util/version';
|
import { isAlpha, isBeta } from '../util/version';
|
||||||
|
|
||||||
import * as packageJson from '../../package.json';
|
import * as packageJson from '../../package.json';
|
||||||
import { getSignatureFileName } from './signature';
|
import {
|
||||||
|
hexToBinary,
|
||||||
|
verifySignature,
|
||||||
|
getSignatureFileName,
|
||||||
|
} from './signature';
|
||||||
import { isPathInside } from '../util/isPathInside';
|
import { isPathInside } from '../util/isPathInside';
|
||||||
|
import type { SettingsChannel } from '../main/settingsChannel';
|
||||||
|
|
||||||
import type { LoggerType } from '../types/Logging';
|
import type { LoggerType } from '../types/Logging';
|
||||||
|
|
||||||
|
@ -46,6 +53,8 @@ export const GOT_CONNECT_TIMEOUT = 2 * 60 * 1000;
|
||||||
export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000;
|
export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000;
|
||||||
export const GOT_SOCKET_TIMEOUT = 2 * 60 * 1000;
|
export const GOT_SOCKET_TIMEOUT = 2 * 60 * 1000;
|
||||||
|
|
||||||
|
const INTERVAL = 30 * durations.MINUTE;
|
||||||
|
|
||||||
type JSONUpdateSchema = {
|
type JSONUpdateSchema = {
|
||||||
version: string;
|
version: string;
|
||||||
files: Array<{
|
files: Array<{
|
||||||
|
@ -59,50 +68,315 @@ type JSONUpdateSchema = {
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdaterInterface = {
|
|
||||||
force(): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateInformationType = {
|
export type UpdateInformationType = {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
size: number;
|
size: number;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function checkForUpdates(
|
export abstract class Updater {
|
||||||
logger: LoggerType,
|
protected fileName: string | undefined;
|
||||||
forceUpdate = false
|
|
||||||
): Promise<UpdateInformationType | null> {
|
|
||||||
const yaml = await getUpdateYaml();
|
|
||||||
const parsedYaml = parseYaml(yaml);
|
|
||||||
const version = getVersion(parsedYaml);
|
|
||||||
|
|
||||||
if (!version) {
|
protected version: string | undefined;
|
||||||
logger.warn('checkForUpdates: no version extracted from downloaded yaml');
|
|
||||||
|
protected updateFilePath: string | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly logger: LoggerType,
|
||||||
|
private readonly settingsChannel: SettingsChannel,
|
||||||
|
protected readonly getMainWindow: () => BrowserWindow | undefined
|
||||||
|
) {}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Public APIs
|
||||||
|
//
|
||||||
|
|
||||||
|
public async force(): Promise<void> {
|
||||||
|
return this.checkForUpdatesMaybeInstall(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
this.logger.info('updater/start: starting checks...');
|
||||||
|
|
||||||
|
app.once('quit', () => this.quitHandler());
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await this.checkForUpdatesMaybeInstall();
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`updater/start: ${Errors.toLogFormat(error)}`);
|
||||||
|
}
|
||||||
|
}, INTERVAL);
|
||||||
|
|
||||||
|
await this.deletePreviousInstallers();
|
||||||
|
await this.checkForUpdatesMaybeInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
public quitHandler(): void {
|
||||||
|
if (this.updateFilePath) {
|
||||||
|
this.deleteCache(this.updateFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Abstract methods
|
||||||
|
//
|
||||||
|
|
||||||
|
protected abstract deletePreviousInstallers(): Promise<void>;
|
||||||
|
|
||||||
|
protected abstract installUpdate(updateFilePath: string): Promise<void>;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Protected methods
|
||||||
|
//
|
||||||
|
|
||||||
|
protected setUpdateListener(performUpdateCallback: () => void): void {
|
||||||
|
ipcMain.removeAllListeners('start-update');
|
||||||
|
ipcMain.once('start-update', performUpdateCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private methods
|
||||||
|
//
|
||||||
|
|
||||||
|
private async downloadAndInstall(
|
||||||
|
newFileName: string,
|
||||||
|
newVersion: string,
|
||||||
|
updateOnProgress?: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
const { logger } = this;
|
||||||
|
try {
|
||||||
|
const oldFileName = this.fileName;
|
||||||
|
const oldVersion = this.version;
|
||||||
|
|
||||||
|
if (this.updateFilePath) {
|
||||||
|
this.deleteCache(this.updateFilePath);
|
||||||
|
}
|
||||||
|
this.fileName = newFileName;
|
||||||
|
this.version = newVersion;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.updateFilePath = await this.downloadUpdate(
|
||||||
|
this.fileName,
|
||||||
|
updateOnProgress
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Restore state in case of download error
|
||||||
|
this.fileName = oldFileName;
|
||||||
|
this.version = oldVersion;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = hexToBinary(config.get('updatesPublicKey'));
|
||||||
|
const verified = await verifySignature(
|
||||||
|
this.updateFilePath,
|
||||||
|
this.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: '${this.version}'; fileName: '${this.fileName}')`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.installUpdate(this.updateFilePath);
|
||||||
|
|
||||||
|
const mainWindow = this.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('show-update-dialog', DialogType.Update, {
|
||||||
|
version: this.version,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
'downloadAndInstall: no mainWindow, cannot show update dialog'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`downloadAndInstall: ${Errors.toLogFormat(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkForUpdatesMaybeInstall(force = false): Promise<void> {
|
||||||
|
const { logger } = this;
|
||||||
|
|
||||||
|
logger.info('checkForUpdatesMaybeInstall: checking for update...');
|
||||||
|
const result = await this.checkForUpdates(force);
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fileName: newFileName, version: newVersion } = result;
|
||||||
|
|
||||||
|
if (
|
||||||
|
force ||
|
||||||
|
this.fileName !== newFileName ||
|
||||||
|
!this.version ||
|
||||||
|
gt(newVersion, this.version)
|
||||||
|
) {
|
||||||
|
const autoDownloadUpdates = await this.getAutoDownloadUpdateSetting();
|
||||||
|
if (!autoDownloadUpdates) {
|
||||||
|
this.setUpdateListener(async () => {
|
||||||
|
logger.info(
|
||||||
|
'checkForUpdatesMaybeInstall: have not downloaded update, going to download'
|
||||||
|
);
|
||||||
|
await this.downloadAndInstall(newFileName, newVersion, true);
|
||||||
|
});
|
||||||
|
const mainWindow = this.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 this.downloadAndInstall(newFileName, newVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkForUpdates(
|
||||||
|
forceUpdate = false
|
||||||
|
): Promise<UpdateInformationType | null> {
|
||||||
|
const yaml = await getUpdateYaml();
|
||||||
|
const parsedYaml = parseYaml(yaml);
|
||||||
|
const version = getVersion(parsedYaml);
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
this.logger.warn(
|
||||||
|
'checkForUpdates: no version extracted from downloaded yaml'
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forceUpdate || isVersionNewer(version)) {
|
||||||
|
this.logger.info(
|
||||||
|
`checkForUpdates: found newer version ${version} ` +
|
||||||
|
`forceUpdate=${forceUpdate}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileName = getUpdateFileName(parsedYaml);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileName,
|
||||||
|
size: getSize(parsedYaml, fileName),
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(
|
||||||
|
`checkForUpdates: ${version} is not newer; no new update available`
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceUpdate || isVersionNewer(version)) {
|
private async downloadUpdate(
|
||||||
logger.info(
|
fileName: string,
|
||||||
`checkForUpdates: found newer version ${version} ` +
|
updateOnProgress?: boolean
|
||||||
`forceUpdate=${forceUpdate}`
|
): Promise<string> {
|
||||||
);
|
const baseUrl = getUpdatesBase();
|
||||||
|
const updateFileUrl = `${baseUrl}/${fileName}`;
|
||||||
|
|
||||||
const fileName = getUpdateFileName(parsedYaml);
|
const signatureFileName = getSignatureFileName(fileName);
|
||||||
|
const signatureUrl = `${baseUrl}/${signatureFileName}`;
|
||||||
|
|
||||||
return {
|
let tempDir;
|
||||||
fileName,
|
try {
|
||||||
size: getSize(parsedYaml, fileName),
|
tempDir = await createTempDir();
|
||||||
version,
|
const targetUpdatePath = join(tempDir, fileName);
|
||||||
};
|
const targetSignaturePath = join(tempDir, getSignatureFileName(fileName));
|
||||||
|
|
||||||
|
validatePath(tempDir, targetUpdatePath);
|
||||||
|
validatePath(tempDir, targetSignaturePath);
|
||||||
|
|
||||||
|
this.logger.info(`downloadUpdate: Downloading signature ${signatureUrl}`);
|
||||||
|
const { body } = await got.get(signatureUrl, getGotOptions());
|
||||||
|
await writeFile(targetSignaturePath, body);
|
||||||
|
|
||||||
|
this.logger.info(`downloadUpdate: Downloading update ${updateFileUrl}`);
|
||||||
|
const downloadStream = got.stream(updateFileUrl, getGotOptions());
|
||||||
|
const writeStream = createWriteStream(targetUpdatePath);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const mainWindow = this.getMainWindow();
|
||||||
|
if (updateOnProgress && 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);
|
||||||
|
});
|
||||||
|
downloadStream.on('end', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.on('error', error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadStream.pipe(writeStream);
|
||||||
|
});
|
||||||
|
|
||||||
|
return targetUpdatePath;
|
||||||
|
} catch (error) {
|
||||||
|
if (tempDir) {
|
||||||
|
await deleteTempDir(tempDir);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
private async getAutoDownloadUpdateSetting(): Promise<boolean> {
|
||||||
`checkForUpdates: ${version} is not newer; no new update available`
|
try {
|
||||||
);
|
return await this.settingsChannel.getSettingFromMainWindow(
|
||||||
|
'autoDownloadUpdate'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
'getAutoDownloadUpdateSetting: Failed to fetch, returning false',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
private async deleteCache(filePath: string | null): Promise<void> {
|
||||||
|
if (!filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tempDir = dirname(filePath);
|
||||||
|
try {
|
||||||
|
await deleteTempDir(tempDir);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`quitHandler: ${Errors.toLogFormat(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validatePath(basePath: string, targetPath: string): void {
|
export function validatePath(basePath: string, targetPath: string): void {
|
||||||
|
@ -115,75 +389,6 @@ export function validatePath(basePath: string, targetPath: string): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadUpdate(
|
|
||||||
fileName: string,
|
|
||||||
logger: LoggerType,
|
|
||||||
mainWindow?: BrowserWindow
|
|
||||||
): Promise<string> {
|
|
||||||
const baseUrl = getUpdatesBase();
|
|
||||||
const updateFileUrl = `${baseUrl}/${fileName}`;
|
|
||||||
|
|
||||||
const signatureFileName = getSignatureFileName(fileName);
|
|
||||||
const signatureUrl = `${baseUrl}/${signatureFileName}`;
|
|
||||||
|
|
||||||
let tempDir;
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
const targetUpdatePath = join(tempDir, fileName);
|
|
||||||
const targetSignaturePath = join(tempDir, getSignatureFileName(fileName));
|
|
||||||
|
|
||||||
validatePath(tempDir, targetUpdatePath);
|
|
||||||
validatePath(tempDir, targetSignaturePath);
|
|
||||||
|
|
||||||
logger.info(`downloadUpdate: Downloading signature ${signatureUrl}`);
|
|
||||||
const { body } = await got.get(signatureUrl, getGotOptions());
|
|
||||||
await writeFile(targetSignaturePath, body);
|
|
||||||
|
|
||||||
logger.info(`downloadUpdate: Downloading update ${updateFileUrl}`);
|
|
||||||
const downloadStream = got.stream(updateFileUrl, getGotOptions());
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
downloadStream.on('end', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
writeStream.on('error', error => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadStream.pipe(writeStream);
|
|
||||||
});
|
|
||||||
|
|
||||||
return targetUpdatePath;
|
|
||||||
} catch (error) {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
export function getUpdateCheckUrl(): string {
|
export function getUpdateCheckUrl(): string {
|
||||||
|
@ -338,13 +543,6 @@ export async function deleteTempDir(targetDir: string): Promise<void> {
|
||||||
await rimrafPromise(targetDir);
|
await rimrafPromise(targetDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrintableError(error: Error | string): Error | string {
|
|
||||||
if (typeof error === 'string') {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
return error && error.stack ? error.stack : error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCliOptions<T>(options: ParserConfiguration['options']): T {
|
export function getCliOptions<T>(options: ParserConfiguration['options']): T {
|
||||||
const parser = createParser({ options });
|
const parser = createParser({ options });
|
||||||
const cliOptions = parser.parse(process.argv);
|
const cliOptions = parser.parse(process.argv);
|
||||||
|
@ -357,34 +555,3 @@ export function getCliOptions<T>(options: ParserConfiguration['options']): T {
|
||||||
|
|
||||||
return (cliOptions as unknown) as T;
|
return (cliOptions as unknown) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUpdateListener(performUpdateCallback: () => void): void {
|
|
||||||
ipcMain.removeAllListeners('start-update');
|
|
||||||
ipcMain.once('start-update', performUpdateCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAutoDownloadUpdateSetting(
|
|
||||||
mainWindow: BrowserWindow | undefined,
|
|
||||||
logger: LoggerType
|
|
||||||
): Promise<boolean> {
|
|
||||||
if (!mainWindow) {
|
|
||||||
logger.warn(
|
|
||||||
'getAutoDownloadUpdateSetting: No main window, returning false'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { getCliOptions, getPrintableError } from './common';
|
import * as Errors from '../types/errors';
|
||||||
|
import { getCliOptions } from './common';
|
||||||
import { keyPair } from './curve';
|
import { keyPair } from './curve';
|
||||||
import { writeHexToPath } from './signature';
|
import { writeHexToPath } from './signature';
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ type OptionsType = {
|
||||||
|
|
||||||
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
||||||
go(cliOptions).catch(error => {
|
go(cliOptions).catch(error => {
|
||||||
console.error('Something went wrong!', getPrintableError(error));
|
console.error('Something went wrong!', Errors.toLogFormat(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function go(options: OptionsType) {
|
async function go(options: OptionsType) {
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { readdir as readdirCallback } from 'fs';
|
||||||
|
|
||||||
import pify from 'pify';
|
import pify from 'pify';
|
||||||
|
|
||||||
import { getCliOptions, getPrintableError } from './common';
|
import * as Errors from '../types/errors';
|
||||||
|
import { getCliOptions } from './common';
|
||||||
import { writeSignature } from './signature';
|
import { writeSignature } from './signature';
|
||||||
import * as packageJson from '../../package.json';
|
import * as packageJson from '../../package.json';
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ type OptionsType = {
|
||||||
|
|
||||||
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
||||||
go(cliOptions).catch(error => {
|
go(cliOptions).catch(error => {
|
||||||
console.error('Something went wrong!', getPrintableError(error));
|
console.error('Something went wrong!', Errors.toLogFormat(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function go(options: OptionsType) {
|
async function go(options: OptionsType) {
|
||||||
|
|
|
@ -4,18 +4,20 @@
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
import type { UpdaterInterface } from './common';
|
import type { Updater } from './common';
|
||||||
import { start as startMacOS } from './macos';
|
import { MacOSUpdater } from './macos';
|
||||||
import { start as startWindows } from './windows';
|
import { WindowsUpdater } from './windows';
|
||||||
import type { LoggerType } from '../types/Logging';
|
import type { LoggerType } from '../types/Logging';
|
||||||
|
import type { SettingsChannel } from '../main/settingsChannel';
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
let updater: UpdaterInterface | undefined;
|
let updater: Updater | undefined;
|
||||||
|
|
||||||
export async function start(
|
export async function start(
|
||||||
getMainWindow: () => BrowserWindow | undefined,
|
settingsChannel: SettingsChannel,
|
||||||
logger?: LoggerType
|
logger: LoggerType,
|
||||||
|
getMainWindow: () => BrowserWindow | undefined
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { platform } = process;
|
const { platform } = process;
|
||||||
|
|
||||||
|
@ -37,12 +39,14 @@ export async function start(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
updater = await startWindows(getMainWindow, logger);
|
updater = new WindowsUpdater(logger, settingsChannel, getMainWindow);
|
||||||
} else if (platform === 'darwin') {
|
} else if (platform === 'darwin') {
|
||||||
updater = await startMacOS(getMainWindow, logger);
|
updater = new MacOSUpdater(logger, settingsChannel, getMainWindow);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('updater/start: Unsupported platform');
|
throw new Error('updater/start: Unsupported platform');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await updater.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function force(): Promise<void> {
|
export async function force(): Promise<void> {
|
||||||
|
|
|
@ -5,169 +5,31 @@ import { createReadStream, statSync } from 'fs';
|
||||||
import type { IncomingMessage, Server, ServerResponse } from 'http';
|
import type { IncomingMessage, Server, ServerResponse } from 'http';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
import type { BrowserWindow } from 'electron';
|
import { autoUpdater } from 'electron';
|
||||||
import { app, autoUpdater } from 'electron';
|
|
||||||
import config from 'config';
|
|
||||||
import { gt } from 'semver';
|
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
|
|
||||||
import type { UpdaterInterface } from './common';
|
import { Updater } from './common';
|
||||||
import {
|
import { explodePromise } from '../util/explodePromise';
|
||||||
checkForUpdates,
|
import * as Errors from '../types/errors';
|
||||||
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 { markShouldQuit } from '../../app/window_state';
|
import { markShouldQuit } from '../../app/window_state';
|
||||||
import { DialogType } from '../types/Dialogs';
|
import { DialogType } from '../types/Dialogs';
|
||||||
|
|
||||||
const INTERVAL = 30 * durations.MINUTE;
|
export class MacOSUpdater extends Updater {
|
||||||
|
protected async deletePreviousInstallers(): Promise<void> {
|
||||||
export async function start(
|
// No installers are cache on macOS
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fileName: newFileName, version: newVersion } = result;
|
protected async installUpdate(updateFilePath: string): Promise<void> {
|
||||||
|
const { logger } = this;
|
||||||
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}')`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handToAutoUpdate(updateFilePath, logger);
|
await this.handToAutoUpdate(updateFilePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const readOnly = 'Cannot update while running on a read-only volume';
|
const readOnly = 'Cannot update while running on a read-only volume';
|
||||||
const message: string = error.message || '';
|
const message: string = error.message || '';
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = this.getMainWindow();
|
||||||
if (mainWindow && message.includes(readOnly)) {
|
if (mainWindow && message.includes(readOnly)) {
|
||||||
logger.info('downloadAndInstall: showing read-only dialog...');
|
logger.info('downloadAndInstall: showing read-only dialog...');
|
||||||
mainWindow.webContents.send(
|
mainWindow.webContents.send(
|
||||||
|
@ -195,55 +57,25 @@ async function downloadAndInstall(
|
||||||
// because Squirrel has cached the update file and will do the right thing.
|
// because Squirrel has cached the update file and will do the right thing.
|
||||||
logger.info('downloadAndInstall: showing update dialog...');
|
logger.info('downloadAndInstall: showing update dialog...');
|
||||||
|
|
||||||
setUpdateListener(() => {
|
this.setUpdateListener(() => {
|
||||||
logger.info('performUpdate: calling quitAndInstall...');
|
logger.info('performUpdate: calling quitAndInstall...');
|
||||||
markShouldQuit();
|
markShouldQuit();
|
||||||
autoUpdater.quitAndInstall();
|
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() {
|
private async handToAutoUpdate(filePath: string): Promise<void> {
|
||||||
deleteCache(updateFilePath, loggerForQuitHandler);
|
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 token = getGuid();
|
||||||
const updateFileUrl = generateFileUrl();
|
const updateFileUrl = generateFileUrl();
|
||||||
const server = createServer();
|
const server = createServer();
|
||||||
let serverUrl: string;
|
let serverUrl: string;
|
||||||
|
|
||||||
server.on('error', (error: Error) => {
|
server.on('error', (error: Error) => {
|
||||||
logger.error(`handToAutoUpdate: ${getPrintableError(error)}`);
|
logger.error(`handToAutoUpdate: ${Errors.toLogFormat(error)}`);
|
||||||
shutdown(server, logger);
|
this.shutdown(server);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -266,12 +98,15 @@ async function handToAutoUpdate(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url || !url.startsWith(updateFileUrl)) {
|
if (!url || !url.startsWith(updateFileUrl)) {
|
||||||
write404(url, response, logger);
|
this.logger.error(
|
||||||
|
`write404: Squirrel requested unexpected url '${url}'`
|
||||||
|
);
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeUpdateToSquirrel(filePath, server, response, logger, reject);
|
this.pipeUpdateToSquirrel(filePath, server, response, reject);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -280,14 +115,14 @@ async function handToAutoUpdate(
|
||||||
serverUrl = getServerUrl(server);
|
serverUrl = getServerUrl(server);
|
||||||
|
|
||||||
autoUpdater.on('error', (...args) => {
|
autoUpdater.on('error', (...args) => {
|
||||||
logger.error('autoUpdater: error', ...args.map(getPrintableError));
|
logger.error('autoUpdater: error', ...args.map(Errors.toLogFormat));
|
||||||
|
|
||||||
const [error] = args;
|
const [error] = args;
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
autoUpdater.on('update-downloaded', () => {
|
autoUpdater.on('update-downloaded', () => {
|
||||||
logger.info('autoUpdater: update-downloaded event fired');
|
logger.info('autoUpdater: update-downloaded event fired');
|
||||||
shutdown(server, logger);
|
this.shutdown(server);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -307,46 +142,75 @@ async function handToAutoUpdate(
|
||||||
reject(error);
|
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(
|
// Helpers
|
||||||
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) {
|
function writeJSONResponse(url: string, response: ServerResponse) {
|
||||||
const data = Buffer.from(
|
const data = Buffer.from(
|
||||||
|
@ -374,16 +238,6 @@ function writeTokenResponse(token: string, response: ServerResponse) {
|
||||||
response.end(data);
|
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) {
|
function getServerUrl(server: Server) {
|
||||||
const address = server.address() as AddressInfo;
|
const address = server.address() as AddressInfo;
|
||||||
|
|
||||||
|
@ -398,27 +252,3 @@ function getFileSize(targetPath: string): number {
|
||||||
|
|
||||||
return size;
|
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)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,172 +1,60 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
// Copyright 2019-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { dirname, join } from 'path';
|
import { join } from 'path';
|
||||||
import type { SpawnOptions } from 'child_process';
|
import type { SpawnOptions } from 'child_process';
|
||||||
import { spawn as spawnEmitter } from 'child_process';
|
import { spawn as spawnEmitter } from 'child_process';
|
||||||
import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs';
|
import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs';
|
||||||
|
|
||||||
import type { BrowserWindow } from 'electron';
|
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import config from 'config';
|
|
||||||
import { gt } from 'semver';
|
|
||||||
import pify from 'pify';
|
import pify from 'pify';
|
||||||
|
|
||||||
import type { UpdaterInterface } from './common';
|
import { Updater } 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 { markShouldQuit } from '../../app/window_state';
|
import { markShouldQuit } from '../../app/window_state';
|
||||||
import { DialogType } from '../types/Dialogs';
|
import { DialogType } from '../types/Dialogs';
|
||||||
|
|
||||||
const readdir = pify(readdirCallback);
|
const readdir = pify(readdirCallback);
|
||||||
const unlink = pify(unlinkCallback);
|
const unlink = pify(unlinkCallback);
|
||||||
|
|
||||||
const INTERVAL = 30 * durations.MINUTE;
|
const IS_EXE = /\.exe$/i;
|
||||||
|
|
||||||
let fileName: string;
|
export class WindowsUpdater extends Updater {
|
||||||
let version: string;
|
private installing = false;
|
||||||
let updateFilePath: string;
|
|
||||||
let installing: boolean;
|
|
||||||
let loggerForQuitHandler: LoggerType;
|
|
||||||
|
|
||||||
export async function start(
|
// This is fixed by our new install mechanisms...
|
||||||
getMainWindow: () => BrowserWindow | undefined,
|
// https://github.com/signalapp/Signal-Desktop/issues/2369
|
||||||
logger: LoggerType
|
// ...but we should also clean up those old installers.
|
||||||
): Promise<UpdaterInterface> {
|
protected async deletePreviousInstallers(): Promise<void> {
|
||||||
logger.info('windows/start: starting checks...');
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
loggerForQuitHandler = logger;
|
const fullPath = join(userDataPath, file);
|
||||||
app.once('quit', quitHandler);
|
try {
|
||||||
|
await unlink(fullPath);
|
||||||
setInterval(async () => {
|
} catch (error) {
|
||||||
try {
|
this.logger.error(
|
||||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
`deletePreviousInstallers: couldn't delete file ${file}`
|
||||||
} catch (error) {
|
);
|
||||||
logger.error(`windows/start: ${getPrintableError(error)}`);
|
}
|
||||||
}
|
})
|
||||||
}, INTERVAL);
|
|
||||||
|
|
||||||
await deletePreviousInstallers(logger);
|
|
||||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
|
||||||
|
|
||||||
return {
|
|
||||||
async force(): Promise<void> {
|
|
||||||
return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, not showing update dialog'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
|
|
||||||
}
|
}
|
||||||
}
|
protected async installUpdate(updateFilePath: string): Promise<void> {
|
||||||
|
const { logger } = this;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
`Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('downloadAndInstall: showing dialog...');
|
logger.info('downloadAndInstall: showing dialog...');
|
||||||
setUpdateListener(async () => {
|
this.setUpdateListener(async () => {
|
||||||
try {
|
try {
|
||||||
await verifyAndInstall(updateFilePath, newVersion, logger);
|
await this.install(updateFilePath);
|
||||||
installing = true;
|
this.installing = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const mainWindow = getMainWindow();
|
const mainWindow = this.getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
logger.info(
|
logger.info(
|
||||||
'createUpdater: showing general update failure dialog...'
|
'createUpdater: showing general update failure dialog...'
|
||||||
|
@ -185,110 +73,41 @@ async function downloadAndInstall(
|
||||||
markShouldQuit();
|
markShouldQuit();
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainWindow = getMainWindow();
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('show-update-dialog', DialogType.Update, {
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
'downloadAndInstall: no mainWindow, cannot show update dialog'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function quitHandler() {
|
private async install(filePath: string): Promise<void> {
|
||||||
if (updateFilePath && !installing) {
|
if (this.installing) {
|
||||||
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
|
return;
|
||||||
error => {
|
}
|
||||||
loggerForQuitHandler.error(`quitHandler: ${getPrintableError(error)}`);
|
|
||||||
|
const { logger } = this;
|
||||||
|
|
||||||
|
logger.info('windows/install: installing package...');
|
||||||
|
const args = ['--updated'];
|
||||||
|
const options = {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore' as const, // 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// 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
|
|
||||||
) {
|
|
||||||
if (installing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = hexToBinary(config.get('updatesPublicKey'));
|
|
||||||
const verified = await 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 const, // 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: ${getPrintableError(error)}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getElevatePath() {
|
function getElevatePath() {
|
||||||
const installPath = app.getAppPath();
|
const installPath = app.getAppPath();
|
||||||
|
|
||||||
|
|
|
@ -54,30 +54,12 @@ export function createSetting<
|
||||||
|
|
||||||
function getValue(): Promise<Value> {
|
function getValue(): Promise<Value> {
|
||||||
strictAssert(options.getter, `${name} has no getter`);
|
strictAssert(options.getter, `${name} has no getter`);
|
||||||
return new Promise((resolve, reject) => {
|
return ipcRenderer.invoke(`settings:get:${name}`);
|
||||||
ipcRenderer.once(`settings:get-success:${name}`, (_, error, value) => {
|
|
||||||
if (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(value);
|
|
||||||
});
|
|
||||||
ipcRenderer.send(`settings:get:${name}`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setValue(value: Value): Promise<Value> {
|
function setValue(value: Value): Promise<Value> {
|
||||||
strictAssert(options.setter, `${name} has no setter`);
|
strictAssert(options.setter, `${name} has no setter`);
|
||||||
return new Promise((resolve, reject) => {
|
return ipcRenderer.invoke(`settings:set:${name}`, value);
|
||||||
ipcRenderer.once(`settings:set-success:${name}`, (_, error) => {
|
|
||||||
if (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(value);
|
|
||||||
});
|
|
||||||
ipcRenderer.send(`settings:set:${name}`, value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -98,16 +80,7 @@ export function createCallback<
|
||||||
name: Name
|
name: Name
|
||||||
): (...args: Parameters<Callback>) => Promise<UnwrapReturn<Callback>> {
|
): (...args: Parameters<Callback>) => Promise<UnwrapReturn<Callback>> {
|
||||||
return (...args: Parameters<Callback>): Promise<UnwrapReturn<Callback>> => {
|
return (...args: Parameters<Callback>): Promise<UnwrapReturn<Callback>> => {
|
||||||
return new Promise<UnwrapReturn<Callback>>((resolve, reject) => {
|
return ipcRenderer.invoke(`settings:call:${name}`, args);
|
||||||
ipcRenderer.once(`callbacks:call-success:${name}`, (_, error, value) => {
|
|
||||||
if (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(value);
|
|
||||||
});
|
|
||||||
ipcRenderer.send(`callbacks:call:${name}`, args);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +92,7 @@ export function installSetting(
|
||||||
const setterName = getSetterName(name);
|
const setterName = getSetterName(name);
|
||||||
|
|
||||||
if (getter) {
|
if (getter) {
|
||||||
ipcRenderer.on(`settings:get:${name}`, async () => {
|
ipcRenderer.on(`settings:get:${name}`, async (_event, { seq }) => {
|
||||||
const getFn = window.Events[getterName];
|
const getFn = window.Events[getterName];
|
||||||
if (!getFn) {
|
if (!getFn) {
|
||||||
ipcRenderer.send(
|
ipcRenderer.send(
|
||||||
|
@ -129,10 +102,11 @@ export function installSetting(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ipcRenderer.send(`settings:get-success:${name}`, null, await getFn());
|
ipcRenderer.send('settings:response', seq, null, await getFn());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ipcRenderer.send(
|
ipcRenderer.send(
|
||||||
`settings:get-success:${name}`,
|
'settings:response',
|
||||||
|
seq,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -140,7 +114,7 @@ export function installSetting(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setter) {
|
if (setter) {
|
||||||
ipcRenderer.on(`settings:set:${name}`, async (_event, value: unknown) => {
|
ipcRenderer.on(`settings:set:${name}`, async (_event, { seq, value }) => {
|
||||||
// Some settings do not have setters...
|
// Some settings do not have setters...
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const setFn = (window.Events as any)[setterName] as (
|
const setFn = (window.Events as any)[setterName] as (
|
||||||
|
@ -148,17 +122,19 @@ export function installSetting(
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
if (!setFn) {
|
if (!setFn) {
|
||||||
ipcRenderer.send(
|
ipcRenderer.send(
|
||||||
`settings:set-success:${name}`,
|
'settings:response',
|
||||||
|
seq,
|
||||||
`installSetter: ${setterName} not found for event ${name}`
|
`installSetter: ${setterName} not found for event ${name}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await setFn(value);
|
await setFn(value);
|
||||||
ipcRenderer.send(`settings:set-success:${name}`);
|
ipcRenderer.send('settings:response', seq, null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ipcRenderer.send(
|
ipcRenderer.send(
|
||||||
`settings:set-success:${name}`,
|
'settings:response',
|
||||||
|
seq,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -169,19 +145,16 @@ export function installSetting(
|
||||||
export function installCallback<Name extends keyof IPCEventsCallbacksType>(
|
export function installCallback<Name extends keyof IPCEventsCallbacksType>(
|
||||||
name: Name
|
name: Name
|
||||||
): void {
|
): void {
|
||||||
ipcRenderer.on(`callbacks:call:${name}`, async (_, args) => {
|
ipcRenderer.on(`settings:call:${name}`, async (_, { seq, args }) => {
|
||||||
const hook = window.Events[name] as (
|
const hook = window.Events[name] as (
|
||||||
...hookArgs: Array<unknown>
|
...hookArgs: Array<unknown>
|
||||||
) => Promise<unknown>;
|
) => Promise<unknown>;
|
||||||
try {
|
try {
|
||||||
ipcRenderer.send(
|
ipcRenderer.send('settings:response', seq, null, await hook(...args));
|
||||||
`callbacks:call-success:${name}`,
|
|
||||||
null,
|
|
||||||
await hook(...args)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ipcRenderer.send(
|
ipcRenderer.send(
|
||||||
`callbacks:call-success:${name}`,
|
'settings:response',
|
||||||
|
seq,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,32 +67,7 @@ installSetting('preferredAudioInputDevice');
|
||||||
installSetting('preferredAudioOutputDevice');
|
installSetting('preferredAudioOutputDevice');
|
||||||
installSetting('preferredVideoInputDevice');
|
installSetting('preferredVideoInputDevice');
|
||||||
|
|
||||||
window.getMediaPermissions = () =>
|
window.getMediaPermissions = () => ipc.invoke('settings:get:mediaPermissions');
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
ipc.once(
|
|
||||||
'settings:get-success:mediaPermissions',
|
|
||||||
(_event, error, value) => {
|
|
||||||
if (error) {
|
|
||||||
return reject(new Error(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
ipc.send('settings:get:mediaPermissions');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.getMediaCameraPermissions = () =>
|
window.getMediaCameraPermissions = () =>
|
||||||
new Promise((resolve, reject) => {
|
ipc.invoke('settings:get:mediaCameraPermissions');
|
||||||
ipc.once(
|
|
||||||
'settings:get-success:mediaCameraPermissions',
|
|
||||||
(_event, error, value) => {
|
|
||||||
if (error) {
|
|
||||||
return reject(new Error(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(value);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
ipc.send('settings:get:mediaCameraPermissions');
|
|
||||||
});
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue