| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- 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');
- import { Event } from './Event';
- 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 {
- public onDownloadProgress: Event<IStreamProgress> = new Event('downloadProgress');
- 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', this.onDownloadProgress.trigger);
- await new Promise((resolve, reject) => {
- stream.pipe(progress)
- .pipe(createWriteStream(path))
- .on('finish', resolve);
- });
- }
- }
|