Enable auto-updating using electron-updater
parent
06142c880a
commit
178d788dca
@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"updatesEnabled": true
|
||||||
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
provider: s3
|
owner: <yourGHName>
|
||||||
region: us-east-1
|
repo: <yourGHRepoName>
|
||||||
bucket: your-test-bucket.signal.org
|
provider: github
|
||||||
path: desktop
|
|
||||||
acl: public-read
|
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import { assert } from 'chai';
|
|
||||||
|
|
||||||
import { getUpdateFileName, getVersion } from '../../updater/common';
|
|
||||||
|
|
||||||
describe('updater/signatures', () => {
|
|
||||||
const windows = `version: 1.23.2
|
|
||||||
files:
|
|
||||||
- url: signal-desktop-win-1.23.2.exe
|
|
||||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
|
||||||
size: 92020776
|
|
||||||
path: signal-desktop-win-1.23.2.exe
|
|
||||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
|
||||||
releaseDate: '2019-03-29T16:58:08.210Z'
|
|
||||||
`;
|
|
||||||
const mac = `version: 1.23.2
|
|
||||||
files:
|
|
||||||
- url: signal-desktop-mac-1.23.2.zip
|
|
||||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
|
||||||
size: 105179791
|
|
||||||
blockMapSize: 111109
|
|
||||||
path: signal-desktop-mac-1.23.2.zip
|
|
||||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
|
||||||
releaseDate: '2019-03-29T16:57:16.997Z'
|
|
||||||
`;
|
|
||||||
const windowsBeta = `version: 1.23.2-beta.1
|
|
||||||
files:
|
|
||||||
- url: signal-desktop-beta-win-1.23.2-beta.1.exe
|
|
||||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
|
||||||
size: 92028656
|
|
||||||
path: signal-desktop-beta-win-1.23.2-beta.1.exe
|
|
||||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
|
||||||
releaseDate: '2019-03-29T01:56:00.544Z'
|
|
||||||
`;
|
|
||||||
const macBeta = `version: 1.23.2-beta.1
|
|
||||||
files:
|
|
||||||
- url: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
|
||||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
|
||||||
size: 105182398
|
|
||||||
blockMapSize: 110894
|
|
||||||
path: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
|
||||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
|
||||||
releaseDate: '2019-03-29T01:53:23.881Z'
|
|
||||||
`;
|
|
||||||
|
|
||||||
describe('#getVersion', () => {
|
|
||||||
it('successfully gets version', () => {
|
|
||||||
const expected = '1.23.2';
|
|
||||||
assert.strictEqual(getVersion(windows), expected);
|
|
||||||
assert.strictEqual(getVersion(mac), expected);
|
|
||||||
|
|
||||||
const expectedBeta = '1.23.2-beta.1';
|
|
||||||
assert.strictEqual(getVersion(windowsBeta), expectedBeta);
|
|
||||||
assert.strictEqual(getVersion(macBeta), expectedBeta);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#getUpdateFileName', () => {
|
|
||||||
it('successfully gets version', () => {
|
|
||||||
assert.strictEqual(
|
|
||||||
getUpdateFileName(windows),
|
|
||||||
'signal-desktop-win-1.23.2.exe'
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getUpdateFileName(mac),
|
|
||||||
'signal-desktop-mac-1.23.2.zip'
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getUpdateFileName(windowsBeta),
|
|
||||||
'signal-desktop-beta-win-1.23.2-beta.1.exe'
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
getUpdateFileName(macBeta),
|
|
||||||
'signal-desktop-beta-mac-1.23.2-beta.1.zip'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,206 +0,0 @@
|
|||||||
import { existsSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import { copy } from 'fs-extra';
|
|
||||||
|
|
||||||
import {
|
|
||||||
_getFileHash,
|
|
||||||
getSignaturePath,
|
|
||||||
loadHexFromPath,
|
|
||||||
verifySignature,
|
|
||||||
writeHexToPath,
|
|
||||||
writeSignature,
|
|
||||||
} from '../../updater/signature';
|
|
||||||
import { createTempDir, deleteTempDir } from '../../updater/common';
|
|
||||||
import { keyPair } from '../../updater/curve';
|
|
||||||
|
|
||||||
describe('updater/signatures', () => {
|
|
||||||
it('_getFileHash returns correct hash', async () => {
|
|
||||||
const filePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
|
||||||
const expected =
|
|
||||||
'7bc77f27d92d00b4a1d57c480ca86dacc43d57bc318339c92119d1fbf6b557a5';
|
|
||||||
|
|
||||||
const hash = await _getFileHash(filePath);
|
|
||||||
|
|
||||||
assert.strictEqual(expected, Buffer.from(hash).toString('hex'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('roundtrips binary file writes', async () => {
|
|
||||||
let tempDir;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
|
|
||||||
const path = join(tempDir, 'something.bin');
|
|
||||||
const { publicKey } = keyPair();
|
|
||||||
|
|
||||||
await writeHexToPath(path, publicKey);
|
|
||||||
|
|
||||||
const fromDisk = await loadHexFromPath(path);
|
|
||||||
|
|
||||||
assert.strictEqual(
|
|
||||||
Buffer.from(fromDisk).compare(Buffer.from(publicKey)),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('roundtrips signature', async () => {
|
|
||||||
let tempDir;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
|
|
||||||
const version = 'v1.23.2';
|
|
||||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
|
||||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
|
||||||
await copy(sourcePath, updatePath);
|
|
||||||
|
|
||||||
const privateKeyPath = join(tempDir, 'private.key');
|
|
||||||
const { publicKey, privateKey } = keyPair();
|
|
||||||
await writeHexToPath(privateKeyPath, privateKey);
|
|
||||||
|
|
||||||
await writeSignature(updatePath, version, privateKeyPath);
|
|
||||||
|
|
||||||
const signaturePath = getSignaturePath(updatePath);
|
|
||||||
assert.strictEqual(existsSync(signaturePath), true);
|
|
||||||
|
|
||||||
const verified = await verifySignature(updatePath, version, publicKey);
|
|
||||||
assert.strictEqual(verified, true);
|
|
||||||
} finally {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails signature verification if version changes', async () => {
|
|
||||||
let tempDir;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
|
|
||||||
const version = 'v1.23.2';
|
|
||||||
const brokenVersion = 'v1.23.3';
|
|
||||||
|
|
||||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
|
||||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
|
||||||
await copy(sourcePath, updatePath);
|
|
||||||
|
|
||||||
const privateKeyPath = join(tempDir, 'private.key');
|
|
||||||
const { publicKey, privateKey } = keyPair();
|
|
||||||
await writeHexToPath(privateKeyPath, privateKey);
|
|
||||||
|
|
||||||
await writeSignature(updatePath, version, privateKeyPath);
|
|
||||||
|
|
||||||
const verified = await verifySignature(
|
|
||||||
updatePath,
|
|
||||||
brokenVersion,
|
|
||||||
publicKey
|
|
||||||
);
|
|
||||||
assert.strictEqual(verified, false);
|
|
||||||
} finally {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails signature verification if signature tampered with', async () => {
|
|
||||||
let tempDir;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
|
|
||||||
const version = 'v1.23.2';
|
|
||||||
|
|
||||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
|
||||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
|
||||||
await copy(sourcePath, updatePath);
|
|
||||||
|
|
||||||
const privateKeyPath = join(tempDir, 'private.key');
|
|
||||||
const { publicKey, privateKey } = keyPair();
|
|
||||||
await writeHexToPath(privateKeyPath, privateKey);
|
|
||||||
|
|
||||||
await writeSignature(updatePath, version, privateKeyPath);
|
|
||||||
|
|
||||||
const signaturePath = getSignaturePath(updatePath);
|
|
||||||
const signature = Buffer.from(await loadHexFromPath(signaturePath));
|
|
||||||
signature[4] += 3;
|
|
||||||
await writeHexToPath(signaturePath, signature);
|
|
||||||
|
|
||||||
const verified = await verifySignature(updatePath, version, publicKey);
|
|
||||||
assert.strictEqual(verified, false);
|
|
||||||
} finally {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails signature verification if binary file tampered with', async () => {
|
|
||||||
let tempDir;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
|
|
||||||
const version = 'v1.23.2';
|
|
||||||
|
|
||||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
|
||||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
|
||||||
await copy(sourcePath, updatePath);
|
|
||||||
|
|
||||||
const privateKeyPath = join(tempDir, 'private.key');
|
|
||||||
const { publicKey, privateKey } = keyPair();
|
|
||||||
await writeHexToPath(privateKeyPath, privateKey);
|
|
||||||
|
|
||||||
await writeSignature(updatePath, version, privateKeyPath);
|
|
||||||
|
|
||||||
const brokenSourcePath = join(
|
|
||||||
__dirname,
|
|
||||||
'../../../fixtures/pixabay-Soap-Bubble-7141.mp4'
|
|
||||||
);
|
|
||||||
await copy(brokenSourcePath, updatePath);
|
|
||||||
|
|
||||||
const verified = await verifySignature(updatePath, version, publicKey);
|
|
||||||
assert.strictEqual(verified, false);
|
|
||||||
} finally {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails signature verification if signed by different key', async () => {
|
|
||||||
let tempDir;
|
|
||||||
|
|
||||||
try {
|
|
||||||
tempDir = await createTempDir();
|
|
||||||
|
|
||||||
const version = 'v1.23.2';
|
|
||||||
|
|
||||||
const sourcePath = join(__dirname, '../../../fixtures/ghost-kitty.mp4');
|
|
||||||
const updatePath = join(tempDir, 'ghost-kitty.mp4');
|
|
||||||
await copy(sourcePath, updatePath);
|
|
||||||
|
|
||||||
const privateKeyPath = join(tempDir, 'private.key');
|
|
||||||
const { publicKey } = keyPair();
|
|
||||||
const { privateKey } = keyPair();
|
|
||||||
await writeHexToPath(privateKeyPath, privateKey);
|
|
||||||
|
|
||||||
await writeSignature(updatePath, version, privateKeyPath);
|
|
||||||
|
|
||||||
const verified = await verifySignature(updatePath, version, publicKey);
|
|
||||||
assert.strictEqual(verified, false);
|
|
||||||
} finally {
|
|
||||||
if (tempDir) {
|
|
||||||
await deleteTempDir(tempDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,45 +0,0 @@
|
|||||||
import { getCliOptions, getPrintableError } from './common';
|
|
||||||
import { keyPair } from './curve';
|
|
||||||
import { writeHexToPath } from './signature';
|
|
||||||
|
|
||||||
/* tslint:disable:no-console */
|
|
||||||
|
|
||||||
const OPTIONS = [
|
|
||||||
{
|
|
||||||
names: ['help', 'h'],
|
|
||||||
type: 'bool',
|
|
||||||
help: 'Print this help and exit.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
names: ['key', 'k'],
|
|
||||||
type: 'string',
|
|
||||||
help: 'Path where public key will go',
|
|
||||||
default: 'public.key',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
names: ['private', 'p'],
|
|
||||||
type: 'string',
|
|
||||||
help: 'Path where private key will go',
|
|
||||||
default: 'private.key',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type OptionsType = {
|
|
||||||
key: string;
|
|
||||||
private: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
|
||||||
go(cliOptions).catch(error => {
|
|
||||||
console.error('Something went wrong!', getPrintableError(error));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function go(options: OptionsType) {
|
|
||||||
const { key: publicKeyPath, private: privateKeyPath } = options;
|
|
||||||
const { publicKey, privateKey } = keyPair();
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
writeHexToPath(publicKeyPath, publicKey),
|
|
||||||
writeHexToPath(privateKeyPath, privateKey),
|
|
||||||
]);
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import { join, resolve } from 'path';
|
|
||||||
import { readdir as readdirCallback } from 'fs';
|
|
||||||
|
|
||||||
import pify from 'pify';
|
|
||||||
|
|
||||||
import { getCliOptions, getPrintableError } from './common';
|
|
||||||
import { writeSignature } from './signature';
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import * as packageJson from '../../package.json';
|
|
||||||
|
|
||||||
const readdir = pify(readdirCallback);
|
|
||||||
|
|
||||||
/* tslint:disable:no-console */
|
|
||||||
|
|
||||||
const OPTIONS = [
|
|
||||||
{
|
|
||||||
names: ['help', 'h'],
|
|
||||||
type: 'bool',
|
|
||||||
help: 'Print this help and exit.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
names: ['private', 'p'],
|
|
||||||
type: 'string',
|
|
||||||
help: 'Path to private key file (default: ./private.key)',
|
|
||||||
default: 'private.key',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
names: ['update', 'u'],
|
|
||||||
type: 'string',
|
|
||||||
help: 'Path to the update package (default: the .exe or .zip in ./release)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
names: ['version', 'v'],
|
|
||||||
type: 'string',
|
|
||||||
help: `Version number of this package (default: ${packageJson.version})`,
|
|
||||||
default: packageJson.version,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type OptionsType = {
|
|
||||||
private: string;
|
|
||||||
update: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const cliOptions = getCliOptions<OptionsType>(OPTIONS);
|
|
||||||
go(cliOptions).catch(error => {
|
|
||||||
console.error('Something went wrong!', getPrintableError(error));
|
|
||||||
});
|
|
||||||
|
|
||||||
async function go(options: OptionsType) {
|
|
||||||
const { private: privateKeyPath, version } = options;
|
|
||||||
let { update: updatePath } = options;
|
|
||||||
|
|
||||||
if (!updatePath) {
|
|
||||||
updatePath = await findUpdatePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Signing with...');
|
|
||||||
console.log(` version: ${version}`);
|
|
||||||
console.log(` update file: ${updatePath}`);
|
|
||||||
console.log(` private key file: ${privateKeyPath}`);
|
|
||||||
|
|
||||||
await writeSignature(updatePath, version, privateKeyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const IS_EXE = /\.exe$/;
|
|
||||||
const IS_ZIP = /\.zip$/;
|
|
||||||
async function findUpdatePath(): Promise<string> {
|
|
||||||
const releaseDir = resolve('release');
|
|
||||||
const files: Array<string> = await readdir(releaseDir);
|
|
||||||
|
|
||||||
const max = files.length;
|
|
||||||
for (let i = 0; i < max; i += 1) {
|
|
||||||
const file = files[i];
|
|
||||||
const fullPath = join(releaseDir, file);
|
|
||||||
|
|
||||||
if (IS_EXE.test(file) || IS_ZIP.test(file)) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("No suitable file found in 'release' folder!");
|
|
||||||
}
|
|
@ -1,324 +0,0 @@
|
|||||||
import { createReadStream, statSync } from 'fs';
|
|
||||||
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
|
|
||||||
import { AddressInfo } from 'net';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
import { v4 as getGuid } from 'uuid';
|
|
||||||
import { app, autoUpdater, BrowserWindow, dialog } from 'electron';
|
|
||||||
import { get as getFromConfig } from 'config';
|
|
||||||
import { gt } from 'semver';
|
|
||||||
|
|
||||||
import {
|
|
||||||
checkForUpdates,
|
|
||||||
deleteTempDir,
|
|
||||||
downloadUpdate,
|
|
||||||
getPrintableError,
|
|
||||||
LoggerType,
|
|
||||||
MessagesType,
|
|
||||||
showCannotUpdateDialog,
|
|
||||||
showUpdateDialog,
|
|
||||||
} from './common';
|
|
||||||
import { hexToBinary, verifySignature } from './signature';
|
|
||||||
import { markShouldQuit } from '../../app/window_state';
|
|
||||||
|
|
||||||
let isChecking = false;
|
|
||||||
const SECOND = 1000;
|
|
||||||
const MINUTE = SECOND * 60;
|
|
||||||
const INTERVAL = MINUTE * 30;
|
|
||||||
|
|
||||||
export async function start(
|
|
||||||
getMainWindow: () => BrowserWindow,
|
|
||||||
messages: MessagesType,
|
|
||||||
logger: LoggerType
|
|
||||||
) {
|
|
||||||
logger.info('macos/start: starting checks...');
|
|
||||||
|
|
||||||
loggerForQuitHandler = logger;
|
|
||||||
app.once('quit', quitHandler);
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('macos/start: error:', getPrintableError(error));
|
|
||||||
}
|
|
||||||
}, INTERVAL);
|
|
||||||
|
|
||||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName: string;
|
|
||||||
let version: string;
|
|
||||||
let updateFilePath: string;
|
|
||||||
let loggerForQuitHandler: LoggerType;
|
|
||||||
|
|
||||||
async function checkDownloadAndInstall(
|
|
||||||
getMainWindow: () => BrowserWindow,
|
|
||||||
messages: MessagesType,
|
|
||||||
logger: LoggerType
|
|
||||||
) {
|
|
||||||
if (isChecking) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('checkDownloadAndInstall: checking for update...');
|
|
||||||
try {
|
|
||||||
isChecking = true;
|
|
||||||
|
|
||||||
const result = await checkForUpdates(logger);
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { fileName: newFileName, version: newVersion } = result;
|
|
||||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
|
||||||
deleteCache(updateFilePath, logger);
|
|
||||||
fileName = newFileName;
|
|
||||||
version = newVersion;
|
|
||||||
updateFilePath = await downloadUpdate(fileName, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
|
||||||
const verified = verifySignature(updateFilePath, version, publicKey);
|
|
||||||
if (!verified) {
|
|
||||||
// Note: We don't delete the cache here, because we don't want to continually
|
|
||||||
// re-download the broken release. We will download it only once per launch.
|
|
||||||
throw new Error(
|
|
||||||
`checkDownloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handToAutoUpdate(updateFilePath, logger);
|
|
||||||
} catch (error) {
|
|
||||||
const readOnly = 'Cannot update while running on a read-only volume';
|
|
||||||
const message: string = error.message || '';
|
|
||||||
if (message.includes(readOnly)) {
|
|
||||||
logger.info('checkDownloadAndInstall: showing read-only dialog...');
|
|
||||||
await showReadOnlyDialog(getMainWindow(), messages);
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
|
||||||
);
|
|
||||||
await showCannotUpdateDialog(getMainWindow(), messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, closing the app will cause the update to be installed automatically
|
|
||||||
// because Squirrel has cached the update file and will do the right thing.
|
|
||||||
|
|
||||||
logger.info('checkDownloadAndInstall: showing update dialog...');
|
|
||||||
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
|
|
||||||
if (!shouldUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('checkDownloadAndInstall: calling quitAndInstall...');
|
|
||||||
markShouldQuit();
|
|
||||||
autoUpdater.quitAndInstall();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
|
||||||
} finally {
|
|
||||||
isChecking = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function quitHandler() {
|
|
||||||
deleteCache(updateFilePath, loggerForQuitHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
function deleteCache(filePath: string | null, logger: LoggerType) {
|
|
||||||
if (filePath) {
|
|
||||||
const tempDir = dirname(filePath);
|
|
||||||
deleteTempDir(tempDir).catch(error => {
|
|
||||||
logger.error(
|
|
||||||
'quitHandler: error deleting temporary directory:',
|
|
||||||
getPrintableError(error)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handToAutoUpdate(
|
|
||||||
filePath: string,
|
|
||||||
logger: LoggerType
|
|
||||||
): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const updateFileUrl = generateFileUrl();
|
|
||||||
const server = createServer();
|
|
||||||
let serverUrl: string;
|
|
||||||
|
|
||||||
server.on('error', (error: Error) => {
|
|
||||||
logger.error(
|
|
||||||
'handToAutoUpdate: server had error',
|
|
||||||
getPrintableError(error)
|
|
||||||
);
|
|
||||||
shutdown(server, logger);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on(
|
|
||||||
'request',
|
|
||||||
(request: IncomingMessage, response: ServerResponse) => {
|
|
||||||
const { url } = request;
|
|
||||||
|
|
||||||
if (url === '/') {
|
|
||||||
const absoluteUrl = `${serverUrl}${updateFileUrl}`;
|
|
||||||
writeJSONResponse(absoluteUrl, response);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url || !url.startsWith(updateFileUrl)) {
|
|
||||||
write404(url, response, logger);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeUpdateToSquirrel(filePath, server, response, logger, reject);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
serverUrl = getServerUrl(server);
|
|
||||||
|
|
||||||
autoUpdater.on('error', (error: Error) => {
|
|
||||||
logger.error('autoUpdater: error', getPrintableError(error));
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
|
||||||
logger.info('autoUpdater: update-downloaded event fired');
|
|
||||||
shutdown(server, logger);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
autoUpdater.setFeedURL({
|
|
||||||
url: serverUrl,
|
|
||||||
headers: { 'Cache-Control': 'no-cache' },
|
|
||||||
});
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function pipeUpdateToSquirrel(
|
|
||||||
filePath: string,
|
|
||||||
server: Server,
|
|
||||||
response: ServerResponse,
|
|
||||||
logger: LoggerType,
|
|
||||||
reject: (error: Error) => void
|
|
||||||
) {
|
|
||||||
const updateFileSize = getFileSize(filePath);
|
|
||||||
const readStream = createReadStream(filePath);
|
|
||||||
|
|
||||||
response.on('error', (error: Error) => {
|
|
||||||
logger.error(
|
|
||||||
'pipeUpdateToSquirrel: update file download request had an error',
|
|
||||||
getPrintableError(error)
|
|
||||||
);
|
|
||||||
shutdown(server, logger);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
readStream.on('error', (error: Error) => {
|
|
||||||
logger.error(
|
|
||||||
'pipeUpdateToSquirrel: read stream error response:',
|
|
||||||
getPrintableError(error)
|
|
||||||
);
|
|
||||||
shutdown(server, logger, response);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
response.writeHead(200, {
|
|
||||||
'Content-Type': 'application/zip',
|
|
||||||
'Content-Length': updateFileSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
readStream.pipe(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeJSONResponse(url: string, response: ServerResponse) {
|
|
||||||
const data = Buffer.from(
|
|
||||||
JSON.stringify({
|
|
||||||
url,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
response.writeHead(200, {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': data.byteLength,
|
|
||||||
});
|
|
||||||
response.end(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function write404(
|
|
||||||
url: string | undefined,
|
|
||||||
response: ServerResponse,
|
|
||||||
logger: LoggerType
|
|
||||||
) {
|
|
||||||
logger.error(`write404: Squirrel requested unexpected url '${url}'`);
|
|
||||||
response.writeHead(404);
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getServerUrl(server: Server) {
|
|
||||||
const address = server.address() as AddressInfo;
|
|
||||||
|
|
||||||
// tslint:disable-next-line:no-http-string
|
|
||||||
return `http://127.0.0.1:${address.port}`;
|
|
||||||
}
|
|
||||||
function generateFileUrl(): string {
|
|
||||||
return `/${getGuid()}.zip`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFileSize(targetPath: string): number {
|
|
||||||
const { size } = statSync(targetPath);
|
|
||||||
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shutdown(
|
|
||||||
server: Server,
|
|
||||||
logger: LoggerType,
|
|
||||||
response?: ServerResponse
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (server) {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('shutdown: Error closing server', getPrintableError(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (response) {
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
} catch (endError) {
|
|
||||||
logger.error(
|
|
||||||
"shutdown: couldn't end response",
|
|
||||||
getPrintableError(endError)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showReadOnlyDialog(
|
|
||||||
mainWindow: BrowserWindow,
|
|
||||||
messages: MessagesType
|
|
||||||
): Promise<void> {
|
|
||||||
const options = {
|
|
||||||
type: 'warning',
|
|
||||||
buttons: [messages.ok.message],
|
|
||||||
title: messages.cannotUpdate.message,
|
|
||||||
message: messages.readOnlyVolume.message,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
dialog.showMessageBox(mainWindow, options, () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
import { createHash } from 'crypto';
|
|
||||||
import {
|
|
||||||
createReadStream,
|
|
||||||
readFile as readFileCallback,
|
|
||||||
writeFile as writeFileCallback,
|
|
||||||
} from 'fs';
|
|
||||||
import { basename, dirname, join, resolve as resolvePath } from 'path';
|
|
||||||
|
|
||||||
import pify from 'pify';
|
|
||||||
|
|
||||||
import { BinaryType, sign, verify } from './curve';
|
|
||||||
|
|
||||||
const readFile = pify(readFileCallback);
|
|
||||||
const writeFile = pify(writeFileCallback);
|
|
||||||
|
|
||||||
export async function generateSignature(
|
|
||||||
updatePackagePath: string,
|
|
||||||
version: string,
|
|
||||||
privateKeyPath: string
|
|
||||||
) {
|
|
||||||
const privateKey = await loadHexFromPath(privateKeyPath);
|
|
||||||
const message = await generateMessage(updatePackagePath, version);
|
|
||||||
|
|
||||||
return sign(privateKey, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifySignature(
|
|
||||||
updatePackagePath: string,
|
|
||||||
version: string,
|
|
||||||
publicKey: BinaryType
|
|
||||||
): Promise<boolean> {
|
|
||||||
const signaturePath = getSignaturePath(updatePackagePath);
|
|
||||||
const signature = await loadHexFromPath(signaturePath);
|
|
||||||
const message = await generateMessage(updatePackagePath, version);
|
|
||||||
|
|
||||||
return verify(publicKey, message, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
async function generateMessage(
|
|
||||||
updatePackagePath: string,
|
|
||||||
version: string
|
|
||||||
): Promise<BinaryType> {
|
|
||||||
const hash = await _getFileHash(updatePackagePath);
|
|
||||||
const messageString = `${Buffer.from(hash).toString('hex')}-${version}`;
|
|
||||||
|
|
||||||
return Buffer.from(messageString);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeSignature(
|
|
||||||
updatePackagePath: string,
|
|
||||||
version: string,
|
|
||||||
privateKeyPath: string
|
|
||||||
) {
|
|
||||||
const signaturePath = getSignaturePath(updatePackagePath);
|
|
||||||
const signature = await generateSignature(
|
|
||||||
updatePackagePath,
|
|
||||||
version,
|
|
||||||
privateKeyPath
|
|
||||||
);
|
|
||||||
await writeHexToPath(signaturePath, signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function _getFileHash(
|
|
||||||
updatePackagePath: string
|
|
||||||
): Promise<BinaryType> {
|
|
||||||
const hash = createHash('sha256');
|
|
||||||
const stream = createReadStream(updatePackagePath);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stream.on('data', data => {
|
|
||||||
hash.update(data);
|
|
||||||
});
|
|
||||||
stream.on('close', () => {
|
|
||||||
resolve(hash.digest());
|
|
||||||
});
|
|
||||||
stream.on('error', error => {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSignatureFileName(fileName: string) {
|
|
||||||
return `${fileName}.sig`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSignaturePath(updatePackagePath: string): string {
|
|
||||||
const updateFullPath = resolvePath(updatePackagePath);
|
|
||||||
const updateDir = dirname(updateFullPath);
|
|
||||||
const updateFileName = basename(updateFullPath);
|
|
||||||
|
|
||||||
return join(updateDir, getSignatureFileName(updateFileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hexToBinary(target: string): BinaryType {
|
|
||||||
return Buffer.from(target, 'hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function binaryToHex(data: BinaryType): string {
|
|
||||||
return Buffer.from(data).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHexFromPath(target: string): Promise<BinaryType> {
|
|
||||||
const hexString = await readFile(target, 'utf8');
|
|
||||||
|
|
||||||
return hexToBinary(hexString);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeHexToPath(target: string, data: BinaryType) {
|
|
||||||
await writeFile(target, binaryToHex(data));
|
|
||||||
}
|
|
@ -0,0 +1,77 @@
|
|||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
import { markShouldQuit } from '../../app/window_state';
|
||||||
|
import {
|
||||||
|
getPrintableError,
|
||||||
|
LoggerType,
|
||||||
|
MessagesType,
|
||||||
|
showCannotUpdateDialog,
|
||||||
|
showUpdateDialog,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
|
let isUpdating = false;
|
||||||
|
|
||||||
|
const SECOND = 1000;
|
||||||
|
const MINUTE = SECOND * 60;
|
||||||
|
const INTERVAL = MINUTE * 30;
|
||||||
|
|
||||||
|
export async function start(
|
||||||
|
getMainWindow: () => BrowserWindow,
|
||||||
|
messages: MessagesType,
|
||||||
|
logger: LoggerType
|
||||||
|
) {
|
||||||
|
logger.info('auto-update: starting checks...');
|
||||||
|
|
||||||
|
autoUpdater.logger = logger;
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await checkForUpdates(getMainWindow, messages, logger);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('auto-update: error:', getPrintableError(error));
|
||||||
|
}
|
||||||
|
}, INTERVAL);
|
||||||
|
|
||||||
|
await checkForUpdates(getMainWindow, messages, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates(
|
||||||
|
getMainWindow: () => BrowserWindow,
|
||||||
|
messages: MessagesType,
|
||||||
|
logger: LoggerType
|
||||||
|
) {
|
||||||
|
if (isUpdating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('auto-update: checking for update...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the update using electron-updater
|
||||||
|
try {
|
||||||
|
const info = await autoUpdater.checkForUpdates();
|
||||||
|
if (!info.downloadPromise) {
|
||||||
|
logger.info('auto-update: no update to download');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await info.downloadPromise;
|
||||||
|
} catch (error) {
|
||||||
|
await showCannotUpdateDialog(getMainWindow(), messages);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update downloaded successfully, we should ask the user to update
|
||||||
|
logger.info('auto-update: showing update dialog...');
|
||||||
|
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('auto-update: calling quitAndInstall...');
|
||||||
|
markShouldQuit();
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
} finally {
|
||||||
|
isUpdating = false;
|
||||||
|
}
|
||||||
|
}
|
@ -1,231 +0,0 @@
|
|||||||
import { dirname, join } from 'path';
|
|
||||||
import { spawn as spawnEmitter, SpawnOptions } from 'child_process';
|
|
||||||
import { readdir as readdirCallback, unlink as unlinkCallback } from 'fs';
|
|
||||||
|
|
||||||
import { app, BrowserWindow } from 'electron';
|
|
||||||
import { get as getFromConfig } from 'config';
|
|
||||||
import { gt } from 'semver';
|
|
||||||
import pify from 'pify';
|
|
||||||
|
|
||||||
import {
|
|
||||||
checkForUpdates,
|
|
||||||
deleteTempDir,
|
|
||||||
downloadUpdate,
|
|
||||||
getPrintableError,
|
|
||||||
LoggerType,
|
|
||||||
MessagesType,
|
|
||||||
showCannotUpdateDialog,
|
|
||||||
showUpdateDialog,
|
|
||||||
} from './common';
|
|
||||||
import { hexToBinary, verifySignature } from './signature';
|
|
||||||
import { markShouldQuit } from '../../app/window_state';
|
|
||||||
|
|
||||||
const readdir = pify(readdirCallback);
|
|
||||||
const unlink = pify(unlinkCallback);
|
|
||||||
|
|
||||||
let isChecking = false;
|
|
||||||
const SECOND = 1000;
|
|
||||||
const MINUTE = SECOND * 60;
|
|
||||||
const INTERVAL = MINUTE * 30;
|
|
||||||
|
|
||||||
export async function start(
|
|
||||||
getMainWindow: () => BrowserWindow,
|
|
||||||
messages: MessagesType,
|
|
||||||
logger: LoggerType
|
|
||||||
) {
|
|
||||||
logger.info('windows/start: starting checks...');
|
|
||||||
|
|
||||||
loggerForQuitHandler = logger;
|
|
||||||
app.once('quit', quitHandler);
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
try {
|
|
||||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('windows/start: error:', getPrintableError(error));
|
|
||||||
}
|
|
||||||
}, INTERVAL);
|
|
||||||
|
|
||||||
await deletePreviousInstallers(logger);
|
|
||||||
await checkDownloadAndInstall(getMainWindow, messages, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName: string;
|
|
||||||
let version: string;
|
|
||||||
let updateFilePath: string;
|
|
||||||
let installing: boolean;
|
|
||||||
let loggerForQuitHandler: LoggerType;
|
|
||||||
|
|
||||||
async function checkDownloadAndInstall(
|
|
||||||
getMainWindow: () => BrowserWindow,
|
|
||||||
messages: MessagesType,
|
|
||||||
logger: LoggerType
|
|
||||||
) {
|
|
||||||
if (isChecking) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isChecking = true;
|
|
||||||
|
|
||||||
logger.info('checkDownloadAndInstall: checking for update...');
|
|
||||||
const result = await checkForUpdates(logger);
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { fileName: newFileName, version: newVersion } = result;
|
|
||||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
|
||||||
deleteCache(updateFilePath, logger);
|
|
||||||
fileName = newFileName;
|
|
||||||
version = newVersion;
|
|
||||||
updateFilePath = await downloadUpdate(fileName, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
|
||||||
const verified = verifySignature(updateFilePath, version, publicKey);
|
|
||||||
if (!verified) {
|
|
||||||
// Note: We don't delete the cache here, because we don't want to continually
|
|
||||||
// re-download the broken release. We will download it only once per launch.
|
|
||||||
throw new Error(
|
|
||||||
`Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('checkDownloadAndInstall: showing dialog...');
|
|
||||||
const shouldUpdate = await showUpdateDialog(getMainWindow(), messages);
|
|
||||||
if (!shouldUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await verifyAndInstall(updateFilePath, version, logger);
|
|
||||||
installing = true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.info(
|
|
||||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
|
||||||
);
|
|
||||||
await showCannotUpdateDialog(getMainWindow(), messages);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
markShouldQuit();
|
|
||||||
app.quit();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
|
||||||
} finally {
|
|
||||||
isChecking = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function quitHandler() {
|
|
||||||
if (updateFilePath && !installing) {
|
|
||||||
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
|
|
||||||
error => {
|
|
||||||
loggerForQuitHandler.error(
|
|
||||||
'quitHandler: error installing:',
|
|
||||||
getPrintableError(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
) {
|
|
||||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
|
||||||
const verified = 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 'ignore', // 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: error deleting temporary directory',
|
|
||||||
getPrintableError(error)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
|
|
||||||
// tslint:disable-next-line no-string-based-set-timeout
|
|
||||||
setTimeout(resolve, 200);
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in New Issue