From eff85c38e99a7e11c47c1e40464258dfcae069bd Mon Sep 17 00:00:00 2001 From: develar Date: Fri, 23 Dec 2016 10:27:58 +0100 Subject: [PATCH] feat: NSIS Updater API to Start Downloading Closes #972 --- README.md | 5 +- appveyor.yml | 2 +- circle.yml | 2 +- docker/6/Dockerfile | 2 +- docker/7/Dockerfile | 2 +- docs/Auto Update.md | 39 +++++++++- nsis-auto-updater/src/NsisUpdater.ts | 72 +++++++++++++------ package.json | 4 +- .../out/__snapshots__/nsisUpdaterTest.js.snap | 30 ++++++++ test/src/nsisUpdaterTest.ts | 47 ++++++++---- 10 files changed, 159 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 1686f053dcd..23d564675d8 100755 --- a/README.md +++ b/README.md @@ -72,10 +72,7 @@ Please note that everything is packaged into an asar archive [by default](https: * `.dmg`: macOS installer, required for the initial installation process on macOS. * `-mac.zip`: required for Squirrel.Mac. -To benefit from auto updates, you have to implement and configure Electron's [`autoUpdater`](http://electron.atom.io/docs/latest/api/auto-updater/) module ([example](https://github.com/develar/onshape-desktop-shell/blob/master/src/AppUpdater.ts)). -You also need to deploy your releases to a server. -Consider using [Nuts](https://github.com/GitbookIO/nuts) (uses GitHub as a backend to store the assets), [Electron Release Server](https://github.com/ArekSredzki/electron-release-server) or [Squirrel Updates Server](https://github.com/Aluxian/squirrel-updates-server). -See the [Publishing Artifacts](https://github.com/electron-userland/electron-builder/wiki/Publishing-Artifacts) section of the [Wiki](https://github.com/electron-userland/electron-builder/wiki) for more information on how to configure your CI environment for automated deployments. +See the [Auto Update](https://github.com/electron-userland/electron-builder/wiki/Auto-Update) section of the [Wiki](https://github.com/electron-userland/electron-builder/wiki). # CLI Usage Execute `node_modules/.bin/build --help` to get the actual CLI usage guide. diff --git a/appveyor.yml b/appveyor.yml index 2de3956d695..0b630d336a7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ cache: - '%USERPROFILE%\.electron' environment: - TEST_FILES: BuildTest,extraMetadataTest,filesTest,globTest,nsisUpdaterTest,appxTest,winPackagerTest + TEST_FILES: BuildTest,extraMetadataTest,filesTest,globTest,nsisUpdaterTest install: - ps: Install-Product node 6 x64 diff --git a/circle.yml b/circle.yml index 19a194abd55..a4d8a8ab6a0 100644 --- a/circle.yml +++ b/circle.yml @@ -14,7 +14,7 @@ dependencies: - sudo apt-get install git-lfs=1.3.0 - ssh git@github.com git-lfs-authenticate $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME.git download - git lfs pull - - docker run --rm --env-file ./test/docker-env.list -v ${PWD}:/project -v ~/.electron:/root/.electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/electron-builder:$([ "$CIRCLE_NODE_INDEX" == "2" ] && echo "4" || echo "wine") /bin/bash -c "node ./test/yarn.js && node ./test/yarn.js test" + - docker run --rm --env-file ./test/docker-env.list -v ${PWD}:/project -v ~/.electron:/root/.electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/electron-builder:$([ "$CIRCLE_NODE_INDEX" == "2" ] && echo "6" || echo "wine") /bin/bash -c "node ./test/yarn.js && node ./test/yarn.js test" test: override: diff --git a/docker/6/Dockerfile b/docker/6/Dockerfile index af050434c84..d0409ef6bb1 100644 --- a/docker/6/Dockerfile +++ b/docker/6/Dockerfile @@ -1,6 +1,6 @@ FROM electronuserland/electron-builder:base -ENV NODE_VERSION 6.9.1 +ENV NODE_VERSION 6.9.2 # https://github.com/npm/npm/issues/4531 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ diff --git a/docker/7/Dockerfile b/docker/7/Dockerfile index 96a6466c4d6..e8d9cd73444 100644 --- a/docker/7/Dockerfile +++ b/docker/7/Dockerfile @@ -1,6 +1,6 @@ FROM electronuserland/electron-builder:base -ENV NODE_VERSION 7.2.0 +ENV NODE_VERSION 7.3.0 # https://github.com/npm/npm/issues/4531 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ diff --git a/docs/Auto Update.md b/docs/Auto Update.md index c0e320a901a..56d217f8494 100644 --- a/docs/Auto Update.md +++ b/docs/Auto Update.md @@ -1,3 +1,12 @@ +To benefit from auto updates, you have to implement and configure Electron's [`autoUpdater`](http://electron.atom.io/docs/latest/api/auto-updater/) module ([example](https://github.com/develar/onshape-desktop-shell/blob/master/src/AppUpdater.ts)). + +See the [Publishing Artifacts](https://github.com/electron-userland/electron-builder/wiki/Publishing-Artifacts) section of the [Wiki](https://github.com/electron-userland/electron-builder/wiki) for more information on how to configure your CI environment for automated deployments. + + +**NOTICE**: [macOS auto-update](https://github.com/electron/electron/blob/master/docs/api/auto-updater.md#macos) is not yet simplified. Update providers supported only on Windows. + +## Quick Setup Guide + 1. Install `electron-auto-updater` as app dependency. 2. [Configure publish](https://github.com/electron-userland/electron-builder/wiki/Options#buildpublish). @@ -19,4 +28,32 @@ } ``` -Currently, `generic` (any HTTPS web server), `github` and `bintray` are supported. `latest.yml` will be generated in addition to installer for `generic` and `github` and must be uploaded also (in short: only `bintray` doesn't use `latest.yml` and this file must be not uploaded on Bintray). \ No newline at end of file +Currently, `generic` (any HTTPS web server), `github` and `bintray` are supported. `latest.yml` will be generated in addition to installer for `generic` and `github` and must be uploaded also (in short: only `bintray` doesn't use `latest.yml` and this file must be not uploaded on Bintray). + +## Options + +Name | Default | Description +--------------------|-------------------------|------------ +autoDownload | true | Automatically download an update when it is found. + +## Methods + +The `autoUpdater` object has the following methods: + +### `autoUpdater.setFeedURL(options)` + +* `options` GenericServerOptions | BintrayOptions | GithubOptions — if you want to override configuration in the `app-update.yml`. + +Sets the `options`. Windows-only for now. On macOS please refer [electron setFeedURL reference](https://github.com/electron/electron/blob/master/docs/api/auto-updater.md#autoupdatersetfeedurlurl-requestheaders). + +### `autoUpdater.checkForUpdates(): Promise` + +Asks the server whether there is an update. On macOS you must call `setFeedURL` before using this API. + +### `autoUpdater.quitAndInstall()` + +Restarts the app and installs the update after it has been downloaded. It +should only be called after `update-downloaded` has been emitted. + +**Note:** `autoUpdater.quitAndInstall()` will close all application windows first and only emit `before-quit` event on `app` after that. +This is different from the normal quit event sequence. \ No newline at end of file diff --git a/nsis-auto-updater/src/NsisUpdater.ts b/nsis-auto-updater/src/NsisUpdater.ts index 51d4174d765..aae34e3b9ad 100644 --- a/nsis-auto-updater/src/NsisUpdater.ts +++ b/nsis-auto-updater/src/NsisUpdater.ts @@ -4,11 +4,11 @@ import * as path from "path" import { tmpdir } from "os" import { gt as isVersionGreaterThan, valid as parseVersion } from "semver" import { download } from "../../src/util/httpRequest" -import { Provider, UpdateCheckResult } from "./api" +import { Provider, UpdateCheckResult, FileInfo } from "./api" import { BintrayProvider } from "./BintrayProvider" import BluebirdPromise from "bluebird-lst-c" -import { BintrayOptions, PublishConfiguration, GithubOptions, GenericServerOptions } from "../../src/options/publishOptions" -import { readFile } from "fs-extra-p" +import { BintrayOptions, PublishConfiguration, GithubOptions, GenericServerOptions, VersionInfo } from "../../src/options/publishOptions" +import { readFile, mkdtemp } from "fs-extra-p" import { safeLoad } from "js-yaml" import { GenericProvider } from "./GenericProvider" import { GitHubProvider } from "./GitHubProvider" @@ -16,6 +16,11 @@ import { executorHolder } from "../../src/util/httpExecutor" import { ElectronHttpExecutor } from "./electronHttpExecutor" export class NsisUpdater extends EventEmitter { + /** + * Automatically download an update when it is found. + */ + public autoDownload = true + private setupPath: string | null private updateAvailable = false @@ -29,6 +34,9 @@ export class NsisUpdater extends EventEmitter { private quitHandlerAdded = false + private versionInfo: VersionInfo | null + private fileInfo: FileInfo | null + constructor(options?: PublishConfiguration | BintrayOptions | GithubOptions) { super() @@ -39,7 +47,7 @@ export class NsisUpdater extends EventEmitter { else { this.app = require("electron").app executorHolder.httpExecutor = new ElectronHttpExecutor() - this.untilAppReady = new BluebirdPromise((resolve, reject) => { + this.untilAppReady = new BluebirdPromise(resolve => { if (this.app.isReady()) { resolve() } @@ -55,11 +63,12 @@ export class NsisUpdater extends EventEmitter { } } + //noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols getFeedURL(): string | null | undefined { - return JSON.stringify(this.clientPromise, null, 2) + return "Deprecated. Do not use it." } - setFeedURL(value: string | PublishConfiguration | BintrayOptions | GithubOptions | GenericServerOptions) { + setFeedURL(value: PublishConfiguration | BintrayOptions | GithubOptions | GenericServerOptions) { this.clientPromise = BluebirdPromise.resolve(createClient(value)) } @@ -103,29 +112,50 @@ export class NsisUpdater extends EventEmitter { const fileInfo = await client.getUpdateFile(versionInfo) this.updateAvailable = true + this.versionInfo = versionInfo + this.fileInfo = fileInfo + this.emit("update-available") - const mkdtemp: (prefix: string) => Promise = require("fs-extra-p").mkdtemp + //noinspection ES6MissingAwait return { versionInfo: versionInfo, fileInfo: fileInfo, - downloadPromise: mkdtemp(`${path.join(tmpdir(), "up")}-`) - .then(it => download(fileInfo.url, path.join(it, fileInfo.name), fileInfo.sha2 == null ? null : {sha2: fileInfo.sha2})) - .then(it => { - this.setupPath = it - this.addQuitHandler() - this.emit("update-downloaded", {}, null, versionInfo.version, null, null, () => { - this.quitAndInstall() - }) - return it - }) - .catch(e => { - this.emit("error", e, (e.stack || e).toString()) - throw e - }), + downloadPromise: this.autoDownload ? this.downloadUpdate() : null, } } + /** + * Start downloading update manually. You can use this method if `autoDownload` option is set to `false`. + * @returns {Promise} Path to downloaded file. + */ + async downloadUpdate() { + const versionInfo = this.versionInfo + const fileInfo = this.fileInfo + + if (versionInfo == null || fileInfo == null) { + const message = "Please check update first" + const error = new Error(message) + this.emit("error", error, message) + throw error + } + + return mkdtemp(`${path.join(tmpdir(), "up")}-`) + .then(it => download(fileInfo.url, path.join(it, fileInfo.name), fileInfo.sha2 == null ? null : {sha2: fileInfo.sha2})) + .then(it => { + this.setupPath = it + this.addQuitHandler() + this.emit("update-downloaded", {}, null, versionInfo.version, null, null, () => { + this.quitAndInstall() + }) + return it + }) + .catch(e => { + this.emit("error", e, (e.stack || e).toString()) + throw e + }) + } + private addQuitHandler() { if (this.quitHandlerAdded) { return diff --git a/package.json b/package.json index 379fc5c2bae..81cea15261f 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,11 @@ "scripts": { "compile": "ts-babel . nsis-auto-updater test", "lint": "node ./test/lint.js", - "pretest": "yarn run compile && yarn run lint", + "pretest": "node ./test/yarn.js run compile && node ./test/yarn.js run lint", "check-deps": "node ./test/out/helpers/checkDeps.js", "test": "node ./test/out/helpers/runTests.js", "test-linux": "docker run --rm -ti -v ${PWD}:/project -v ${PWD##*/}-node-modules:/project/node_modules -v ~/.electron:/root/.electron electronuserland/electron-builder:wine /test.sh", - "pack-updater": "cd nsis-auto-updater && yarn --production && cd ..", + "pack-updater": "cd nsis-auto-updater && node ./test/yarn.js --production && cd ..", "semantic-release": "semantic-release pre && npm publish && semantic-release post", "//": "Update wiki if docs changed. Update only if functionalily are generally available (latest release, not next)", "update-wiki": "git subtree split -b wiki --prefix docs/ && git push -f wiki wiki:master", diff --git a/test/out/__snapshots__/nsisUpdaterTest.js.snap b/test/out/__snapshots__/nsisUpdaterTest.js.snap index 67382edc4a9..ab67cb60ccf 100644 --- a/test/out/__snapshots__/nsisUpdaterTest.js.snap +++ b/test/out/__snapshots__/nsisUpdaterTest.js.snap @@ -6,6 +6,21 @@ Object { } `; +exports[`test file url generic - manual download 1`] = ` +Object { + "name": "TestApp Setup 1.1.0.exe", + "sha2": "f2ca1bb6c7e907d06dafe4687e579fce76b37e4e93b7605022da52e6ccc26fd2", + "url": "https://develar.s3.amazonaws.com/test/TestApp Setup 1.1.0.exe", +} +`; + +exports[`test file url generic - manual download 2`] = ` +Array [ + "checking-for-update", + "update-available", +] +`; + exports[`test file url generic 1`] = ` Object { "name": "TestApp Setup 1.1.0.exe", @@ -14,6 +29,14 @@ Object { } `; +exports[`test file url generic 2`] = ` +Array [ + "checking-for-update", + "update-available", + "update-downloaded", +] +`; + exports[`test file url github 1`] = ` Object { "name": "TestApp-Setup-1.1.0.exe", @@ -21,3 +44,10 @@ Object { "url": "https://github.com/develar/__test_nsis_release/releases/download/v1.1.0/TestApp-Setup-1.1.0.exe", } `; + +exports[`test test error 1`] = ` +Array [ + "checking-for-update", + "error", +] +`; diff --git a/test/src/nsisUpdaterTest.ts b/test/src/nsisUpdaterTest.ts index 220184e3282..a939ccb9feb 100644 --- a/test/src/nsisUpdaterTest.ts +++ b/test/src/nsisUpdaterTest.ts @@ -81,19 +81,34 @@ test("file url generic", async () => { g.__test_resourcesPath = testResourcesPath const updater: NsisUpdater = new NsisUpdaterClass() - const actualEvents: Array = [] - const expectedEvents = ["checking-for-update", "update-available", "update-downloaded"] - for (const eventName of expectedEvents) { - updater.addListener(eventName, () => { - actualEvents.push(eventName) - }) - } + const actualEvents = trackEvents(updater) const updateCheckResult = await updater.checkForUpdates() expect(updateCheckResult.fileInfo).toMatchSnapshot() await assertThat(path.join(await updateCheckResult.downloadPromise)).isFile() - expect(actualEvents).toEqual(expectedEvents) + expect(actualEvents).toMatchSnapshot() +}) + +test("file url generic - manual download", async () => { + const tmpDir = new TmpDir() + const testResourcesPath = await tmpDir.getTempFile("update-config") + await outputFile(path.join(testResourcesPath, "app-update.yml"), safeDump({ + provider: "generic", + url: "https://develar.s3.amazonaws.com/test", + })) + g.__test_resourcesPath = testResourcesPath + const updater: NsisUpdater = new NsisUpdaterClass() + updater.autoDownload = false + + const actualEvents = trackEvents(updater) + + const updateCheckResult = await updater.checkForUpdates() + expect(updateCheckResult.fileInfo).toMatchSnapshot() + expect(updateCheckResult.downloadPromise).toBeNull() + expect(actualEvents).toMatchSnapshot() + + await assertThat(path.join(await updater.downloadUpdate())).isFile() }) test("file url github", async () => { @@ -126,14 +141,18 @@ test("test error", async () => { g.__test_resourcesPath = null const updater: NsisUpdater = new NsisUpdaterClass() + const actualEvents = trackEvents(updater) + + await assertThat(updater.checkForUpdates()).throws("Path must be a string. Received undefined") + expect(actualEvents).toMatchSnapshot() +}) + +function trackEvents(updater: NsisUpdater) { const actualEvents: Array = [] - const expectedEvents = ["checking-for-update", "error", "error"] - for (const eventName of expectedEvents) { + for (const eventName of ["checking-for-update", "update-available", "update-downloaded", "error"]) { updater.addListener(eventName, () => { actualEvents.push(eventName) }) } - - await assertThat(updater.checkForUpdates()).throws("Path must be a string. Received undefined") - expect(actualEvents).toEqual(expectedEvents) -}) \ No newline at end of file + return actualEvents +} \ No newline at end of file