2023-01-03 19:55:46 +00:00
|
|
|
// Copyright 2019 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
import { join } from 'path';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { SpawnOptions } from 'child_process';
|
|
|
|
import { spawn as spawnEmitter } from 'child_process';
|
2019-03-28 17:09:26 +00:00
|
|
|
import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs';
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import { app } from 'electron';
|
2019-03-28 17:09:26 +00:00
|
|
|
import pify from 'pify';
|
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
import { Updater } from './common';
|
2019-03-28 17:09:26 +00:00
|
|
|
|
|
|
|
const readdir = pify(readdirCallback);
|
|
|
|
const unlink = pify(unlinkCallback);
|
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
const IS_EXE = /\.exe$/i;
|
2021-08-19 22:56:29 +00:00
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
export class WindowsUpdater extends Updater {
|
|
|
|
private installing = false;
|
|
|
|
|
|
|
|
// This is fixed by our new install mechanisms...
|
|
|
|
// https://github.com/signalapp/Signal-Desktop/issues/2369
|
|
|
|
// ...but we should also clean up those old installers.
|
|
|
|
protected async deletePreviousInstallers(): Promise<void> {
|
|
|
|
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;
|
|
|
|
}
|
2021-08-19 22:56:29 +00:00
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
const fullPath = join(userDataPath, file);
|
|
|
|
try {
|
|
|
|
await unlink(fullPath);
|
|
|
|
} catch (error) {
|
|
|
|
this.logger.error(
|
|
|
|
`deletePreviousInstallers: couldn't delete file ${file}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})
|
2021-08-19 22:56:29 +00:00
|
|
|
);
|
|
|
|
}
|
2023-12-12 02:15:36 +00:00
|
|
|
protected async installUpdate(
|
|
|
|
updateFilePath: string,
|
|
|
|
isSilent: boolean
|
|
|
|
): Promise<void> {
|
2021-11-10 00:56:56 +00:00
|
|
|
const { logger } = this;
|
2019-03-28 17:09:26 +00:00
|
|
|
|
2023-12-12 02:15:36 +00:00
|
|
|
const doInstall = async () => {
|
2023-01-23 16:56:39 +00:00
|
|
|
logger.info('downloadAndInstall: installing...');
|
2021-08-23 22:45:11 +00:00
|
|
|
try {
|
2023-12-12 02:15:36 +00:00
|
|
|
await this.install(updateFilePath, isSilent);
|
2021-11-10 00:56:56 +00:00
|
|
|
this.installing = true;
|
2021-08-23 22:45:11 +00:00
|
|
|
} catch (error) {
|
2022-03-28 19:05:44 +00:00
|
|
|
this.markCannotUpdate(error);
|
2021-08-23 22:45:11 +00:00
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
2024-02-27 00:18:50 +00:00
|
|
|
// If interrupted at this point, we only want to restart (not reattempt install)
|
|
|
|
this.setUpdateListener(this.restart);
|
|
|
|
this.restart();
|
2023-12-12 02:15:36 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if (isSilent) {
|
|
|
|
logger.info('downloadAndInstall: running immediately...');
|
|
|
|
await doInstall();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setUpdateListener(doInstall);
|
2021-11-10 00:56:56 +00:00
|
|
|
}
|
2021-10-01 18:49:59 +00:00
|
|
|
|
2024-02-27 00:18:50 +00:00
|
|
|
protected restart(): void {
|
|
|
|
this.logger.info('downloadAndInstall: restarting...');
|
|
|
|
this.markRestarting();
|
|
|
|
app.quit();
|
|
|
|
}
|
|
|
|
|
2023-12-12 02:15:36 +00:00
|
|
|
private async install(filePath: string, isSilent: boolean): Promise<void> {
|
2021-11-10 00:56:56 +00:00
|
|
|
if (this.installing) {
|
|
|
|
return;
|
2021-10-01 18:49:59 +00:00
|
|
|
}
|
2019-03-28 17:09:26 +00:00
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
const { logger } = this;
|
2019-03-28 17:09:26 +00:00
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
logger.info('windows/install: installing package...');
|
|
|
|
const args = ['--updated'];
|
2023-12-12 02:15:36 +00:00
|
|
|
if (isSilent) {
|
|
|
|
// App isn't automatically restarted with "/S" flag, but "--updated"
|
|
|
|
// will trigger our code in `build/installer.nsh` that will start the app
|
|
|
|
// with "--start-in-tray" flag (see `app/main.ts`)
|
|
|
|
args.push('/S');
|
|
|
|
}
|
2021-11-10 00:56:56 +00:00
|
|
|
const options = {
|
|
|
|
detached: true,
|
|
|
|
stdio: 'ignore' as const, // TypeScript considers this a plain string without help
|
|
|
|
};
|
2019-03-28 17:09:26 +00:00
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
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);
|
2019-03-28 17:09:26 +00:00
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
return;
|
2019-03-28 17:09:26 +00:00
|
|
|
}
|
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
throw error;
|
2019-03-28 17:09:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-10 00:56:56 +00:00
|
|
|
// Helpers
|
|
|
|
|
2019-03-28 17:09:26 +00:00
|
|
|
function getElevatePath() {
|
|
|
|
const installPath = app.getAppPath();
|
|
|
|
|
|
|
|
return join(installPath, 'resources', 'elevate.exe');
|
|
|
|
}
|
|
|
|
|
|
|
|
async function spawn(
|
|
|
|
exe: string,
|
|
|
|
args: Array<string>,
|
|
|
|
options: SpawnOptions
|
|
|
|
): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const emitter = spawnEmitter(exe, args, options);
|
|
|
|
emitter.on('error', reject);
|
|
|
|
emitter.unref();
|
|
|
|
|
|
|
|
setTimeout(resolve, 200);
|
|
|
|
});
|
|
|
|
}
|