|
@@ -0,0 +1,284 @@
|
|
|
|
|
+
|
|
|
|
|
+import { dirname } from 'path';
|
|
|
|
|
+import { resolve as urlResolve } from 'url';
|
|
|
|
|
+import { lstat, readFile, createReadStream, createWriteStream, Stats, ReadStream } from 'fs';
|
|
|
|
|
+import { IncomingMessage } from 'http';
|
|
|
|
|
+import { createHash } from 'crypto';
|
|
|
|
|
+import { spawn } from 'child_process';
|
|
|
|
|
+
|
|
|
|
|
+import * as semver from 'semver';
|
|
|
|
|
+import * as tmp from 'tmp';
|
|
|
|
|
+
|
|
|
|
|
+const debug = require('debug/src/browser')('nsis-compat-updater');
|
|
|
|
|
+const got = require('got');
|
|
|
|
|
+const progressStream = require('progress-stream');
|
|
|
|
|
+
|
|
|
|
|
+interface IInstaller {
|
|
|
|
|
+ arch: string;
|
|
|
|
|
+ path: string;
|
|
|
|
|
+ hash: string;
|
|
|
|
|
+ created: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IUpdater {
|
|
|
|
|
+ arch: string;
|
|
|
|
|
+ fromVersion: string;
|
|
|
|
|
+ path: string;
|
|
|
|
|
+ hash: string;
|
|
|
|
|
+ created: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IVersion {
|
|
|
|
|
+ version: string;
|
|
|
|
|
+ changelog: string;
|
|
|
|
|
+ source: string;
|
|
|
|
|
+ installers: IInstaller[];
|
|
|
|
|
+ updaters: IUpdater[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IVersionInfo {
|
|
|
|
|
+ latest: string;
|
|
|
|
|
+ versions: IVersion[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IStreamProgress {
|
|
|
|
|
+ percentage: number;
|
|
|
|
|
+ transferred: number;
|
|
|
|
|
+ length: number;
|
|
|
|
|
+ remaining: number;
|
|
|
|
|
+ eta: number;
|
|
|
|
|
+ runtime: number;
|
|
|
|
|
+ delta: number;
|
|
|
|
|
+ speed: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export class NsisCompatUpdater {
|
|
|
|
|
+
|
|
|
|
|
+ protected versionInfo: IVersionInfo;
|
|
|
|
|
+
|
|
|
|
|
+ constructor(protected seed: string, protected currentVersion: string, protected currentArch: 'x86' | 'x64') {
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public async checkForUpdates(): Promise<IVersion> {
|
|
|
|
|
+
|
|
|
|
|
+ debug('in checkForUpdates');
|
|
|
|
|
+
|
|
|
|
|
+ const versionInfo = await this.getVersionInfo();
|
|
|
|
|
+
|
|
|
|
|
+ if(!semver.gt(versionInfo.latest, this.currentVersion)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return await this.getVersion(versionInfo.latest);
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public async downloadUpdate(version: string): Promise<string> {
|
|
|
|
|
+
|
|
|
|
|
+ debug('in downloadUpdate', 'version', version);
|
|
|
|
|
+
|
|
|
|
|
+ const { installers, updaters } = await this.getVersion(version);
|
|
|
|
|
+
|
|
|
|
|
+ const { url, hash } = (() => {
|
|
|
|
|
+
|
|
|
|
|
+ const updater = updaters.filter(updater => updater.fromVersion == this.currentVersion && updater.arch == this.currentArch)[0];
|
|
|
|
|
+
|
|
|
|
|
+ if(updater) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ url: `${ urlResolve(dirname(this.seed), updater.path) }`,
|
|
|
|
|
+ hash: updater.hash,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const installer = installers.filter(installer => installer.arch == this.currentArch)[0];
|
|
|
|
|
+
|
|
|
|
|
+ if(installer) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ url: `${ urlResolve(dirname(this.seed), installer.path) }`,
|
|
|
|
|
+ hash: installer.hash,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ throw new Error('ERROR_UPDATER_NOT_FOUND');
|
|
|
|
|
+
|
|
|
|
|
+ })();
|
|
|
|
|
+
|
|
|
|
|
+ const path = await this.tmpUpdateFile();
|
|
|
|
|
+
|
|
|
|
|
+ debug('in downloadUpdate', 'url', url);
|
|
|
|
|
+ await this.download(url, path);
|
|
|
|
|
+
|
|
|
|
|
+ debug('in downloadUpdate', 'path', path);
|
|
|
|
|
+ if(!await this.checkFileHash('sha256', path, hash)) {
|
|
|
|
|
+ throw new Error('ERROR_HASH_MISMATCH');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return path;
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public install(path: string, slient: boolean = false) {
|
|
|
|
|
+
|
|
|
|
|
+ debug('in install', 'path', path);
|
|
|
|
|
+ debug(`in install`, 'slient', slient);
|
|
|
|
|
+
|
|
|
|
|
+ const args = [];
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ detached: true,
|
|
|
|
|
+ stdio: 'ignore',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if(slient) {
|
|
|
|
|
+ args.push('/S');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+
|
|
|
|
|
+ spawn(path, args, options)
|
|
|
|
|
+ .unref();
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(err) {
|
|
|
|
|
+
|
|
|
|
|
+ if(err.code == 'UNKNOWN') {
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ // TODO: Elevate and run again.
|
|
|
|
|
+
|
|
|
|
|
+ spawn(elevate, [ path, ...args ], options)
|
|
|
|
|
+ .unref();
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public installWhenQuit(path: string) {
|
|
|
|
|
+
|
|
|
|
|
+ console.info('installWhenQuit');
|
|
|
|
|
+
|
|
|
|
|
+ if((<any>process.versions).nw) {
|
|
|
|
|
+ throw new Error('ERROR_UNKNOWN');
|
|
|
|
|
+ }
|
|
|
|
|
+ else if((<any>process.versions).electron) {
|
|
|
|
|
+ return require('electron').app.on('quit', () => this.install(path, true));
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ throw new Error('ERROR_UNKNOWN');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public quitAndInstall(path: string) {
|
|
|
|
|
+
|
|
|
|
|
+ console.info('quitAndInstall');
|
|
|
|
|
+
|
|
|
|
|
+ if((<any>process.versions).nw) {
|
|
|
|
|
+ this.install(path, false);
|
|
|
|
|
+ nw.App.quit();
|
|
|
|
|
+ }
|
|
|
|
|
+ else if((<any>process.versions).electron) {
|
|
|
|
|
+ return require('electron').app.quit();
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ throw new Error('ERROR_UNKNOWN');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async getVersion(version: string): Promise<IVersion> {
|
|
|
|
|
+
|
|
|
|
|
+ const versionInfo = await this.getVersionInfo();
|
|
|
|
|
+
|
|
|
|
|
+ const item = versionInfo.versions.filter(item => item.version == version)[0];
|
|
|
|
|
+
|
|
|
|
|
+ if(!item) {
|
|
|
|
|
+ throw new Error('ERROR_VERSION_NOT_FOUND');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return item;
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected tmpUpdateFile(): Promise<string> {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ tmp.file(<any>{
|
|
|
|
|
+ postfix: '.exe',
|
|
|
|
|
+ discardDescriptor: true,
|
|
|
|
|
+ }, (err, path, fd, cleanup) => err ? reject(err) : resolve(path));
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected checkFileHash(type: string, path: string, expected: string): Promise<boolean> {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+
|
|
|
|
|
+ const hasher = createHash(type);
|
|
|
|
|
+
|
|
|
|
|
+ hasher.on('error', reject);
|
|
|
|
|
+ hasher.on('readable', () => {
|
|
|
|
|
+
|
|
|
|
|
+ const data = hasher.read();
|
|
|
|
|
+
|
|
|
|
|
+ if(data) {
|
|
|
|
|
+ resolve((<any>data).toString('hex') == expected);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ createReadStream(path).pipe(hasher);
|
|
|
|
|
+
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async getVersionInfo(): Promise<IVersionInfo> {
|
|
|
|
|
+
|
|
|
|
|
+ if(!this.versionInfo) {
|
|
|
|
|
+
|
|
|
|
|
+ const versionInfo = await got(this.seed, {
|
|
|
|
|
+ timeout: 5000,
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res: any) => JSON.parse(res.body));
|
|
|
|
|
+ debug('in getVersionInfo', 'versionInfo', versionInfo);
|
|
|
|
|
+
|
|
|
|
|
+ this.versionInfo = versionInfo;
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return this.versionInfo;
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ protected async download(url: string, path: string, onProgress?: (state: IStreamProgress) => void) {
|
|
|
|
|
+
|
|
|
|
|
+ const stream = got.stream(url);
|
|
|
|
|
+
|
|
|
|
|
+ const size = await new Promise((resolve, reject) => {
|
|
|
|
|
+ stream.on('error', reject);
|
|
|
|
|
+ stream.on('response', resolve);
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((res: IncomingMessage) => res.headers['content-type']);
|
|
|
|
|
+
|
|
|
|
|
+ const progress = progressStream({
|
|
|
|
|
+ length: size,
|
|
|
|
|
+ time: 1000,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ progress.on('progress', onProgress ? onProgress : (state: IStreamProgress) => {
|
|
|
|
|
+ debug('in handleProgress', 'state.speed', state.speed);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
|
|
+ stream.pipe(progress)
|
|
|
|
|
+ .pipe(createWriteStream(path))
|
|
|
|
|
+ .on('finish', resolve);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|