From 8451f55be603c3253525cd13334a38c721944a95 Mon Sep 17 00:00:00 2001 From: evshiron Date: Wed, 19 Apr 2017 11:58:31 +0800 Subject: [PATCH] feat(nsis-compat-updater): initialize --- README.md | 2 +- lerna.json | 7 + package.json | 1 + packages/nsis-compat-tester/app/index.html | 22 ++ packages/nsis-compat-tester/main.html | 65 ++++ packages/nsis-compat-tester/package.json | 30 ++ packages/nsis-compat-updater/package.json | 39 +++ packages/nsis-compat-updater/src/global.d.ts | 2 + packages/nsis-compat-updater/src/index.ts | 0 packages/nsis-compat-updater/src/lib/Event.ts | 22 ++ .../src/lib/NsisCompatUpdater.ts | 284 ++++++++++++++++++ packages/nsis-compat-updater/src/lib/index.ts | 2 + packages/nsis-compat-updater/tsconfig.json | 12 + packages/nsis-compat-updater/tslint.json | 29 ++ 14 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 lerna.json create mode 100644 packages/nsis-compat-tester/app/index.html create mode 100644 packages/nsis-compat-tester/main.html create mode 100644 packages/nsis-compat-tester/package.json create mode 100644 packages/nsis-compat-updater/package.json create mode 100644 packages/nsis-compat-updater/src/global.d.ts create mode 100644 packages/nsis-compat-updater/src/index.ts create mode 100644 packages/nsis-compat-updater/src/lib/Event.ts create mode 100644 packages/nsis-compat-updater/src/lib/NsisCompatUpdater.ts create mode 100644 packages/nsis-compat-updater/src/lib/index.ts create mode 100644 packages/nsis-compat-updater/tsconfig.json create mode 100644 packages/nsis-compat-updater/tslint.json diff --git a/README.md b/README.md index 5adadce..d5200f0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Although NW.js has much lesser popularity than Electron, and is really troubled * Configurable executable fields and icons for Windows and macOS * Integration for `nwjs-ffmpeg-prebuilt` * Exclusion of useless files from `node_modules` -* TODO Auto Updater inspired by `electron-updater` +* [Auto Updater](./packages/nsis-compat-tester/) inspired by `electron-updater` * TODO Rebuilding native modules * Ideas appreciated :) diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..2a5db2c --- /dev/null +++ b/lerna.json @@ -0,0 +1,7 @@ +{ + "lerna": "2.0.0-rc.3", + "packages": [ + "packages/*" + ], + "version": "independent" +} diff --git a/package.json b/package.json index 038c24d..7d48692 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/yargs": "^6.6.0", "ava": "^0.18.2", "cross-env": "^3.2.3", + "lerna": "^2.0.0-rc.3", "nyc": "^10.1.2", "standard-version": "^4.0.0", "tslint": "^4.5.1", diff --git a/packages/nsis-compat-tester/app/index.html b/packages/nsis-compat-tester/app/index.html new file mode 100644 index 0000000..194c069 --- /dev/null +++ b/packages/nsis-compat-tester/app/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/nsis-compat-tester/main.html b/packages/nsis-compat-tester/main.html new file mode 100644 index 0000000..ef17bf1 --- /dev/null +++ b/packages/nsis-compat-tester/main.html @@ -0,0 +1,65 @@ + + + + + + + + + + diff --git a/packages/nsis-compat-tester/package.json b/packages/nsis-compat-tester/package.json new file mode 100644 index 0000000..37230fb --- /dev/null +++ b/packages/nsis-compat-tester/package.json @@ -0,0 +1,30 @@ +{ + "name": "nsis-compat-tester", + "version": "1.0.0", + "description": "", + "main": "main.html", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "serve": "http-server ./dist/", + "start": "DEBUG=nsis-compat-updater run --mirror https://npm.taobao.org/mirrors/nwjs/ .", + "dist": "build --win --x86 --mirror https://npm.taobao.org/mirrors/nwjs/ ." + }, + "author": "evshiron", + "license": "MIT", + "dependencies": { + "nsis-compat-updater": "^1.0.0" + }, + "devDependencies": { + "http-server": "^0.9.0", + "nwjs-builder-phoenix": "^1.9.3" + }, + "build": { + "nwFlavor": "sdk", + "targets": [ + "nsis" + ], + "nsis": { + "diffUpdaters": true + } + } +} diff --git a/packages/nsis-compat-updater/package.json b/packages/nsis-compat-updater/package.json new file mode 100644 index 0000000..c8a1cb0 --- /dev/null +++ b/packages/nsis-compat-updater/package.json @@ -0,0 +1,39 @@ +{ + "name": "nsis-compat-updater", + "version": "1.0.0", + "description": "", + "main": "./dist/lib/index.js", + "scripts": { + "prepublish": "npm run build", + "test": "npm run build && ava --verbose", + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evshiron/nwjs-builder-phoenix.git" + }, + "author": "evshiron", + "license": "MIT", + "bugs": { + "url": "https://github.com/evshiron/nwjs-builder-phoenix/issues" + }, + "homepage": "https://github.com/evshiron/nwjs-builder-phoenix#readme", + "devDependencies": { + "@types/bluebird-global": "^3.5.1", + "@types/node": "^7.0.12", + "@types/semver": "^5.3.31", + "@types/tmp": "0.0.32", + "ava": "^0.18.2", + "nwjs-builder-phoenix": "^1.9.3", + "tslint": "^5.0.0", + "typescript": "^2.2.2" + }, + "dependencies": { + "bluebird": "^3.5.0", + "debug": "^2.6.3", + "got": "^6.7.1", + "progress-stream": "^1.2.0", + "semver": "^5.3.0", + "tmp": "0.0.31" + } +} diff --git a/packages/nsis-compat-updater/src/global.d.ts b/packages/nsis-compat-updater/src/global.d.ts new file mode 100644 index 0000000..05a5916 --- /dev/null +++ b/packages/nsis-compat-updater/src/global.d.ts @@ -0,0 +1,2 @@ + +declare const nw: any; diff --git a/packages/nsis-compat-updater/src/index.ts b/packages/nsis-compat-updater/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/nsis-compat-updater/src/lib/Event.ts b/packages/nsis-compat-updater/src/lib/Event.ts new file mode 100644 index 0000000..5654a2b --- /dev/null +++ b/packages/nsis-compat-updater/src/lib/Event.ts @@ -0,0 +1,22 @@ + +export class Event { + + public listeners: Array<(args: TArgs) => void> = []; + + constructor(name: string) { + + } + + public subscribe(fn: ((args: TArgs) => void)) { + this.listeners.push(fn); + } + + public trigger = (args: TArgs) => { + this.listeners.map(fn => fn(args)); + } + + public unsubscribe(fn: ((args: TArgs) => void)) { + this.listeners = this.listeners.filter(f => f != fn); + } + +} diff --git a/packages/nsis-compat-updater/src/lib/NsisCompatUpdater.ts b/packages/nsis-compat-updater/src/lib/NsisCompatUpdater.ts new file mode 100644 index 0000000..e8e2cb0 --- /dev/null +++ b/packages/nsis-compat-updater/src/lib/NsisCompatUpdater.ts @@ -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 { + + 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 { + + 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((process.versions).nw) { + throw new Error('ERROR_UNKNOWN'); + } + else if((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((process.versions).nw) { + this.install(path, false); + nw.App.quit(); + } + else if((process.versions).electron) { + return require('electron').app.quit(); + } + else { + throw new Error('ERROR_UNKNOWN'); + } + + } + + protected async getVersion(version: string): Promise { + + 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 { + return new Promise((resolve, reject) => { + tmp.file({ + postfix: '.exe', + discardDescriptor: true, + }, (err, path, fd, cleanup) => err ? reject(err) : resolve(path)); + }); + } + + protected checkFileHash(type: string, path: string, expected: string): Promise { + return new Promise((resolve, reject) => { + + const hasher = createHash(type); + + hasher.on('error', reject); + hasher.on('readable', () => { + + const data = hasher.read(); + + if(data) { + resolve((data).toString('hex') == expected); + } + + }); + + createReadStream(path).pipe(hasher); + + }); + } + + protected async getVersionInfo(): Promise { + + 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); + }); + + } + +} diff --git a/packages/nsis-compat-updater/src/lib/index.ts b/packages/nsis-compat-updater/src/lib/index.ts new file mode 100644 index 0000000..2ca63fe --- /dev/null +++ b/packages/nsis-compat-updater/src/lib/index.ts @@ -0,0 +1,2 @@ + +export * from './NsisCompatUpdater'; diff --git a/packages/nsis-compat-updater/tsconfig.json b/packages/nsis-compat-updater/tsconfig.json new file mode 100644 index 0000000..23d060d --- /dev/null +++ b/packages/nsis-compat-updater/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "sourceMap": true, + "noImplicitAny": true, + "module": "commonjs", + "target": "es5" + }, + "include": [ + "./src/**/*" + ] +} diff --git a/packages/nsis-compat-updater/tslint.json b/packages/nsis-compat-updater/tslint.json new file mode 100644 index 0000000..c1e5c80 --- /dev/null +++ b/packages/nsis-compat-updater/tslint.json @@ -0,0 +1,29 @@ +{ + "extends": "tslint:recommended", + "rules": { + "one-line": [ + false + ], + "max-classes-per-file": [ + false + ], + "max-line-length": [ + false + ], + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "whitespace": [ + true, + "check-operator", + "check-decl", + "check-operator", + "check-module", + "check-separator", + "check-type", + "check-preblock" + ] + } +}