Переглянути джерело

feat(nsis-compat-updater): initialize

evshiron 8 роки тому
батько
коміт
8451f55be6

+ 1 - 1
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 :)
 

+ 7 - 0
lerna.json

@@ -0,0 +1,7 @@
+{
+  "lerna": "2.0.0-rc.3",
+  "packages": [
+    "packages/*"
+  ],
+  "version": "independent"
+}

+ 1 - 0
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",

+ 22 - 0
packages/nsis-compat-tester/app/index.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <style type="text/css">
+
+    body {
+        width: 100%;
+        height: 100%;
+        margin: 0;
+    }
+
+    </style>
+</head>
+<body>
+<script type="text/javascript">
+
+document.write(`Version: ${ nw.App.manifest.version }`);
+
+</script>
+</body>
+</html>

+ 65 - 0
packages/nsis-compat-tester/main.html

@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <style type="text/css">
+
+    body {
+        width: 100%;
+        height: 100%;
+        margin: 0;
+    }
+
+    </style>
+</head>
+<body>
+<script type="text/javascript">
+
+document.write(`<p>App launches.</p>`);
+
+const { NsisCompatUpdater } = require('nsis-compat-updater');
+
+const updater = new NsisCompatUpdater('http://127.0.0.1:8080/versions.nsis.json', nw.App.manifest.version, 'x86');
+
+updater.checkForUpdates()
+.then((version) => {
+
+    document.write(`<p>Version: ${ version }</p>`);
+
+    if(version && confirm(`New version of ${ version.version } is available, download now?`)) {
+
+        return updater.downloadUpdate(version.version)
+        .then((path) => {
+
+            document.write(`<p>Path: ${ path }</p>`);
+
+            if(confirm(`New version of ${ version.version } is downloaded, install now?`)) {
+                return updater.quitAndInstall(path);
+            }
+            else {
+                return updater.installWhenQuit(path);
+            }
+
+        });
+
+    }
+    else {
+
+        nw.Window.open('./app/index.html', {
+            width: 640,
+            height: 480,
+            new_instance: true,
+        });
+
+        nw.Window.get().close();
+
+    }
+
+})
+.catch((err) => {
+    document.write(`<p>[ERROR] ${ err.message }</p>`);
+});
+
+</script>
+</body>
+</html>

+ 30 - 0
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
+    }
+  }
+}

+ 39 - 0
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"
+  }
+}

+ 2 - 0
packages/nsis-compat-updater/src/global.d.ts

@@ -0,0 +1,2 @@
+
+declare const nw: any;

+ 0 - 0
packages/nsis-compat-updater/src/index.ts


+ 22 - 0
packages/nsis-compat-updater/src/lib/Event.ts

@@ -0,0 +1,22 @@
+
+export class Event<TArgs> {
+
+    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);
+    }
+
+}

+ 284 - 0
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<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);
+        });
+
+    }
+
+}

+ 2 - 0
packages/nsis-compat-updater/src/lib/index.ts

@@ -0,0 +1,2 @@
+
+export * from './NsisCompatUpdater';

+ 12 - 0
packages/nsis-compat-updater/tsconfig.json

@@ -0,0 +1,12 @@
+{
+    "compilerOptions": {
+        "outDir": "./dist/",
+        "sourceMap": true,
+        "noImplicitAny": true,
+        "module": "commonjs",
+        "target": "es5"
+    },
+    "include": [
+        "./src/**/*"
+    ]
+}

+ 29 - 0
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"
+        ]
+    }
+}