diff --git a/package.json b/package.json index d6bd5f3f88e..269d58f41c0 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "app-builder-bin": "1.3.5", "archiver": "^2.1.1", "async-exit-hook": "^2.0.1", - "aws-sdk": "^2.196.0", + "aws-sdk": "^2.197.0", "bluebird-lst": "^1.0.5", "chalk": "^2.3.0", "chromium-pickle-js": "^0.2.0", @@ -41,7 +41,7 @@ "electron-download-tf": "4.3.4", "electron-is-dev": "^0.3.0", "electron-osx-sign": "0.4.8", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "gitbook-plugin-footer": "^0.0.6", "hosted-git-info": "^2.5.0", "iconv-lite": "^0.4.19", @@ -73,7 +73,7 @@ "@types/ejs": "^2.5.0", "@types/electron-is-dev": "^0.3.0", "@types/ini": "^1.3.29", - "@types/jest": "^22.1.2", + "@types/jest": "^22.1.3", "@types/js-yaml": "^3.10.1", "@types/lodash.isequal": "^4.5.2", "@types/node-emoji": "^1.8.0", @@ -94,7 +94,7 @@ "gitbook-plugin-edit-link": "^2.0.2", "gitbook-plugin-github": "^2.0.0", "gitbook-plugin-github-buttons": "^3.0.0", - "globby": "^8.0.0", + "globby": "^8.0.1", "jest-cli": "^22.3.0", "jest-junit": "^3.6.0", "jsdoc-to-markdown": "^4.0.1", diff --git a/packages/builder-util-runtime/package.json b/packages/builder-util-runtime/package.json index efa0f4b8071..7b67f27134b 100644 --- a/packages/builder-util-runtime/package.json +++ b/packages/builder-util-runtime/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "debug": "^3.1.0", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "bluebird-lst": "^1.0.5", "sax": "^1.2.4" }, diff --git a/packages/builder-util/package.json b/packages/builder-util/package.json index fe1311ddb08..a29429e6236 100644 --- a/packages/builder-util/package.json +++ b/packages/builder-util/package.json @@ -13,7 +13,7 @@ "dependencies": { "app-builder-bin": "1.3.5", "temp-file": "^3.1.1", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "is-ci": "^1.1.0", "stat-mode": "^0.2.2", "bluebird-lst": "^1.0.5", diff --git a/packages/builder-util/src/hash.ts b/packages/builder-util/src/hash.ts index 7478ce86cc1..fbb44d5c93c 100644 --- a/packages/builder-util/src/hash.ts +++ b/packages/builder-util/src/hash.ts @@ -1,9 +1,8 @@ -import BluebirdPromise from "bluebird-lst" import { createHash } from "crypto" import { createReadStream } from "fs" export function hashFile(file: string, algorithm: string = "sha512", encoding: "base64" | "hex" = "base64", options?: any) { - return new BluebirdPromise((resolve, reject) => { + return new Promise((resolve, reject) => { const hash = createHash(algorithm) hash .on("error", reject) diff --git a/packages/dmg-builder/package.json b/packages/dmg-builder/package.json index 21fd06a7022..252f6634fb1 100644 --- a/packages/dmg-builder/package.json +++ b/packages/dmg-builder/package.json @@ -13,7 +13,7 @@ "vendor" ], "dependencies": { - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "bluebird-lst": "^1.0.5", "parse-color": "^1.0.0", "builder-util": "^0.0.0-semantic-release", diff --git a/packages/electron-builder-lib/package.json b/packages/electron-builder-lib/package.json index 1df6fc295f5..d3dbbb3024d 100644 --- a/packages/electron-builder-lib/package.json +++ b/packages/electron-builder-lib/package.json @@ -50,7 +50,7 @@ "builder-util": "0.0.0-semantic-release", "electron-osx-sign": "0.4.8", "electron-publish": "0.0.0-semantic-release", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "hosted-git-info": "^2.5.0", "is-ci": "^1.1.0", "isbinaryfile": "^3.0.2", diff --git a/packages/electron-builder-squirrel-windows/package.json b/packages/electron-builder-squirrel-windows/package.json index 592aa39c1f6..397de8e804c 100644 --- a/packages/electron-builder-squirrel-windows/package.json +++ b/packages/electron-builder-squirrel-windows/package.json @@ -13,7 +13,7 @@ "dependencies": { "builder-util": "^0.0.0-semantic-release", "bluebird-lst": "^1.0.5", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "archiver": "^2.1.1", "sanitize-filename": "^1.6.1" }, diff --git a/packages/electron-builder/package.json b/packages/electron-builder/package.json index 646ee6d690f..a6fba324a49 100644 --- a/packages/electron-builder/package.json +++ b/packages/electron-builder/package.json @@ -50,7 +50,7 @@ "chalk": "^2.3.0", "builder-util-runtime": "0.0.0-semantic-release", "builder-util": "0.0.0-semantic-release", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "is-ci": "^1.1.0", "read-config-file": "3.0.0", "sanitize-filename": "^1.6.1", diff --git a/packages/electron-publish/package.json b/packages/electron-publish/package.json index ed3e822169b..6c67c1be97f 100644 --- a/packages/electron-publish/package.json +++ b/packages/electron-publish/package.json @@ -11,7 +11,7 @@ "out" ], "dependencies": { - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "mime": "^2.2.0", "bluebird-lst": "^1.0.5", "builder-util-runtime": "^0.0.0-semantic-release", diff --git a/packages/electron-publisher-s3/package.json b/packages/electron-publisher-s3/package.json index 474d5ffbc3e..bc08a9dbe35 100644 --- a/packages/electron-publisher-s3/package.json +++ b/packages/electron-publisher-s3/package.json @@ -11,8 +11,8 @@ "out" ], "dependencies": { - "fs-extra-p": "^4.5.0", - "aws-sdk": "^2.196.0", + "fs-extra-p": "^4.5.2", + "aws-sdk": "^2.197.0", "mime": "^2.2.0", "electron-publish": "~0.0.0-semantic-release", "builder-util": "^0.0.0-semantic-release", diff --git a/packages/electron-updater/package.json b/packages/electron-updater/package.json index 151631bb1f1..05c3daa4242 100644 --- a/packages/electron-updater/package.json +++ b/packages/electron-updater/package.json @@ -14,7 +14,7 @@ "dependencies": { "lazy-val": "^1.0.3", "bluebird-lst": "^1.0.5", - "fs-extra-p": "^4.5.0", + "fs-extra-p": "^4.5.2", "js-yaml": "^3.10.0", "semver": "^5.5.0", "source-map-support": "^0.5.3", diff --git a/packages/electron-updater/src/AppImageUpdater.ts b/packages/electron-updater/src/AppImageUpdater.ts index 50471ac6b97..b20f698e415 100644 --- a/packages/electron-updater/src/AppImageUpdater.ts +++ b/packages/electron-updater/src/AppImageUpdater.ts @@ -6,7 +6,7 @@ import * as path from "path" import "source-map-support/register" import { BaseUpdater } from "./BaseUpdater" import { FileWithEmbeddedBlockMapDifferentialDownloader } from "./differentialDownloader/FileWithEmbeddedBlockMapDifferentialDownloader" -import { UPDATE_DOWNLOADED, UpdateCheckResult } from "./main" +import { UpdateCheckResult } from "./main" import { findFile } from "./Provider" export class AppImageUpdater extends BaseUpdater { @@ -45,48 +45,42 @@ export class AppImageUpdater extends BaseUpdater { sha512: fileInfo.info.sha512, } - let installerPath = this.downloadedUpdateHelper.getDownloadedFile(updateInfo, fileInfo) - if (installerPath != null) { - return [installerPath] - } - - await this.executeDownload(downloadOptions, fileInfo, async (tempDir, destinationFile) => { - installerPath = destinationFile - - const oldFile = process.env.APPIMAGE!! - if (oldFile == null) { - throw newError("APPIMAGE env is not defined", "ERR_UPDATER_OLD_FILE_NOT_FOUND") - } - - let isDownloadFull = false - try { - await new FileWithEmbeddedBlockMapDifferentialDownloader(fileInfo.info, this.httpExecutor, { - newUrl: fileInfo.url.href, - oldFile, - logger: this._logger, - newFile: installerPath, - useMultipleRangeRequest: provider.useMultipleRangeRequest, - requestHeaders, - }) - .download() - } - catch (e) { - this._logger.error(`Cannot download differentially, fallback to full download: ${e.stack || e}`) - // during test (developer machine mac) we must throw error - isDownloadFull = process.platform === "linux" - } - - if (isDownloadFull) { - await this.httpExecutor.download(fileInfo.url.href, installerPath, downloadOptions) - } - - await chmod(installerPath, 0o755) + return await this.executeDownload({ + fileExtension: "AppImage", + downloadOptions, + fileInfo, + updateInfo, + task: async updateFile => { + const oldFile = process.env.APPIMAGE!! + if (oldFile == null) { + throw newError("APPIMAGE env is not defined", "ERR_UPDATER_OLD_FILE_NOT_FOUND") + } + + let isDownloadFull = false + try { + await new FileWithEmbeddedBlockMapDifferentialDownloader(fileInfo.info, this.httpExecutor, { + newUrl: fileInfo.url.href, + oldFile, + logger: this._logger, + newFile: updateFile, + useMultipleRangeRequest: provider.useMultipleRangeRequest, + requestHeaders, + }) + .download() + } + catch (e) { + this._logger.error(`Cannot download differentially, fallback to full download: ${e.stack || e}`) + // during test (developer machine mac) we must throw error + isDownloadFull = process.platform === "linux" + } + + if (isDownloadFull) { + await this.httpExecutor.download(fileInfo.url.href, updateFile, downloadOptions) + } + + await chmod(updateFile, 0o755) + }, }) - - this.downloadedUpdateHelper.setDownloadedFile(installerPath!!, null, updateInfo, fileInfo) - this.addQuitHandler() - this.emit(UPDATE_DOWNLOADED, this.updateInfo) - return [installerPath!!] } protected doInstall(installerPath: string, isSilent: boolean, isRunAfter: boolean): boolean { diff --git a/packages/electron-updater/src/AppUpdater.ts b/packages/electron-updater/src/AppUpdater.ts index 0e6883584cd..390647fc096 100644 --- a/packages/electron-updater/src/AppUpdater.ts +++ b/packages/electron-updater/src/AppUpdater.ts @@ -15,6 +15,7 @@ import { ElectronHttpExecutor } from "./electronHttpExecutor" import { GenericProvider } from "./GenericProvider" import { Logger, Provider, UpdateCheckResult, UpdaterSignal } from "./main" import { createClient } from "./providerFactory" +import { DownloadedUpdateHelper } from "./DownloadedUpdateHelper" export abstract class AppUpdater extends EventEmitter { /** @@ -55,6 +56,8 @@ export abstract class AppUpdater extends EventEmitter { private _channel: string | null = null + protected readonly downloadedUpdateHelper: DownloadedUpdateHelper + /** * Get the update channel. Not applicable for GitHub. Doesn't return `channel` from the update configuration, only if was previously set. */ @@ -141,7 +144,7 @@ export abstract class AppUpdater extends EventEmitter { /** @internal */ readonly httpExecutor: ElectronHttpExecutor - protected constructor(options: AllPublishOptions | null | undefined, app?: any) { + protected constructor(options: AllPublishOptions | null | undefined, app?: Electron.App) { super() this.on("error", (error: Error) => { @@ -166,6 +169,8 @@ export abstract class AppUpdater extends EventEmitter { }) } + this.downloadedUpdateHelper = new DownloadedUpdateHelper(this.app.getPath("userData")) + const currentVersionString = this.app.getVersion() const currentVersion = parseVersion(currentVersionString) if (currentVersion == null) { @@ -276,7 +281,9 @@ export abstract class AppUpdater extends EventEmitter { return headers } - private async doCheckForUpdates(): Promise { + protected async getUpdateInfo(): Promise { + await this.untilAppReady + if (this.clientPromise == null) { this.clientPromise = this.configOnDisk.value.then(it => createClient(it, this)) } @@ -284,7 +291,11 @@ export abstract class AppUpdater extends EventEmitter { const client = await this.clientPromise const stagingUserId = await this.stagingUserIdPromise.value client.setRequestHeaders(this.computeFinalHeaders({"X-User-Staging-Id": stagingUserId})) - const updateInfo = await client.getLatestVersion() + return await client.getLatestVersion() + } + + private async doCheckForUpdates(): Promise { + const updateInfo = await this.getUpdateInfo() const latestVersion = parseVersion(updateInfo.version) if (latestVersion == null) { @@ -313,7 +324,7 @@ export abstract class AppUpdater extends EventEmitter { versionInfo: updateInfo, updateInfo, cancellationToken, - downloadPromise: this.autoDownload ? this.downloadUpdate(cancellationToken) : null, + downloadPromise: this.autoDownload ? this.downloadUpdate(cancellationToken) : null } } @@ -430,4 +441,4 @@ export class NoOpLogger implements Logger { error(message?: any) { // ignore } -} +} \ No newline at end of file diff --git a/packages/electron-updater/src/BaseUpdater.ts b/packages/electron-updater/src/BaseUpdater.ts index c8860ce6140..30d81a62b4c 100644 --- a/packages/electron-updater/src/BaseUpdater.ts +++ b/packages/electron-updater/src/BaseUpdater.ts @@ -1,14 +1,10 @@ -import { AllPublishOptions, CancellationError, DownloadOptions } from "builder-util-runtime" -import { mkdtemp, remove } from "fs-extra-p" -import { tmpdir } from "os" +import { UpdateInfo, AllPublishOptions, CancellationError, DownloadOptions } from "builder-util-runtime" +import { ensureDir, rename, unlink } from "fs-extra-p" import * as path from "path" import { AppUpdater } from "./AppUpdater" -import { DownloadedUpdateHelper } from "./DownloadedUpdateHelper" -import { DOWNLOAD_PROGRESS, ResolvedUpdateFileInfo } from "./main" +import { DOWNLOAD_PROGRESS, ResolvedUpdateFileInfo, UPDATE_DOWNLOADED } from "./main" export abstract class BaseUpdater extends AppUpdater { - protected readonly downloadedUpdateHelper = new DownloadedUpdateHelper() - protected quitAndInstallCalled = false private quitHandlerAdded = false @@ -16,59 +12,103 @@ export abstract class BaseUpdater extends AppUpdater { super(options, app) } - quitAndInstall(isSilent: boolean = false, isForceRunAfter: boolean = false): void { + async quitAndInstall(isSilent: boolean = false, isForceRunAfter: boolean = false): Promise { this._logger.info(`Install on explicit quitAndInstall`) - if (this.install(isSilent, isSilent ? isForceRunAfter : true)) { + const isInstalled = await this.install(isSilent, isSilent ? isForceRunAfter : true) + if (isInstalled) { setImmediate(() => { - this.app.quit() + if (this.app.quit !== undefined) { + this.app.quit() + } + this.quitAndInstallCalled = false }) } } - protected async executeDownload(downloadOptions: DownloadOptions, fileInfo: ResolvedUpdateFileInfo, task: (tempDir: string, destinationFile: string, removeTempDirIfAny: () => Promise) => Promise) { + protected async executeDownload(taskOptions: DownloadExecutorTask): Promise> { if (this.listenerCount(DOWNLOAD_PROGRESS) > 0) { - downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it) + taskOptions.downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it) + } + + const updateInfo = taskOptions.updateInfo + const version = updateInfo.version + const fileInfo = taskOptions.fileInfo + const packageInfo = fileInfo.packageInfo + + const cacheDir = this.downloadedUpdateHelper.cacheDir + await ensureDir(cacheDir) + const updateFileName = `installer-${version}.${taskOptions.fileExtension}` + const updateFile = path.join(cacheDir, updateFileName) + const packageFile = packageInfo == null ? null : path.join(cacheDir, `package-${version}.${path.extname(packageInfo.path) || "7z"}`) + + const done = () => { + this.downloadedUpdateHelper.setDownloadedFile(updateFile, packageFile, updateInfo, fileInfo) + this.addQuitHandler() + this.emit(UPDATE_DOWNLOADED, updateInfo) + return packageFile == null ? [updateFile] : [updateFile, packageFile] } - // use TEST_APP_TMP_DIR if defined and developer machine (must be not windows due to security reasons - we must not use env var in the production) - const tempDir = await mkdtemp(`${path.join((process.platform === "darwin" ? process.env.TEST_APP_TMP_DIR : null) || tmpdir(), "up")}-`) + const log = this._logger + if (await this.downloadedUpdateHelper.validateDownloadedPath(updateFile, updateInfo, fileInfo, log)) { + return done() + } - const removeTempDirIfAny = () => { + const removeFileIfAny = () => { this.downloadedUpdateHelper.clear() - return remove(tempDir) + return unlink(updateFile) .catch(() => { // ignored }) } - try { - const destinationFile = path.join(tempDir, path.posix.basename(fileInfo.info.url)) - await task(tempDir, destinationFile, removeTempDirIfAny) + // https://github.com/electron-userland/electron-builder/pull/2474#issuecomment-366481912 + let nameCounter = 0 + let tempUpdateFile = path.join(cacheDir, `temp-${updateFileName}`) + for (let i = 0; i < 3; i++) { + try { + await unlink(tempUpdateFile) + } + catch (e) { + if (e.code === "ENOENT") { + break + } - this._logger.info(`New version ${this.updateInfo!.version} has been downloaded to ${destinationFile}`) + log.warn(`Error on remove temp update file: ${e}`) + tempUpdateFile = path.join(cacheDir, `temp-${nameCounter++}-${updateFileName}`) + } + } + + try { + await taskOptions.task(tempUpdateFile, packageFile, removeFileIfAny) + await rename(tempUpdateFile, updateFile) } catch (e) { - await removeTempDirIfAny() + await removeFileIfAny() if (e instanceof CancellationError) { - this.emit("update-cancelled", this.updateInfo) - this._logger.info("Cancelled") + log.info("Cancelled") + this.emit("update-cancelled", updateInfo) } throw e } + + log.info(`New version ${version} has been downloaded to ${updateFile}`) + return done() } protected abstract doInstall(installerPath: string, isSilent: boolean, isRunAfter: boolean): boolean - protected install(isSilent: boolean, isRunAfter: boolean): boolean { + protected async install(isSilent: boolean, isRunAfter: boolean): Promise { if (this.quitAndInstallCalled) { this._logger.warn("install call ignored: quitAndInstallCalled is set to true") return false } const installerPath = this.downloadedUpdateHelper.file - if (!this.updateAvailable || installerPath == null) { - this.dispatchError(new Error("No update available, can't quit and install")) + // todo check (for now it is ok to no check as before, cached (from previous launch) update file checked in any case) + // const isValid = await this.isUpdateValid(installerPath) + if (installerPath == null) { + this.dispatchError(new Error("No valid update available, can't quit and install")) return false } @@ -92,11 +132,19 @@ export abstract class BaseUpdater extends AppUpdater { this.quitHandlerAdded = true - this.app.once("quit", () => { + this.app.once("quit", async () => { if (!this.quitAndInstallCalled) { this._logger.info("Auto install update on quit") - this.install(true, false) + await this.install(true, false) } }) } +} + +export interface DownloadExecutorTask { + readonly fileExtension: string + readonly downloadOptions: DownloadOptions + readonly fileInfo: ResolvedUpdateFileInfo + readonly updateInfo: UpdateInfo + readonly task: (destinationFile: string, packageFile: string | null, removeTempDirIfAny: () => Promise) => Promise } \ No newline at end of file diff --git a/packages/electron-updater/src/DownloadedUpdateHelper.ts b/packages/electron-updater/src/DownloadedUpdateHelper.ts index b165bce2c47..43678c1d1e3 100644 --- a/packages/electron-updater/src/DownloadedUpdateHelper.ts +++ b/packages/electron-updater/src/DownloadedUpdateHelper.ts @@ -1,44 +1,86 @@ import { UpdateInfo } from "builder-util-runtime" +import { createHash } from "crypto" +import { createReadStream } from "fs" import isEqual from "lodash.isequal" -import { ResolvedUpdateFileInfo } from "./main" +import { Logger, ResolvedUpdateFileInfo } from "./main" +import { pathExists } from "fs-extra-p" /** @private **/ export class DownloadedUpdateHelper { - private setupPath: string | null = null - private _packagePath: string | null = null + private _file: string | null = null + private _packageFile: string | null = null private versionInfo: UpdateInfo | null = null private fileInfo: ResolvedUpdateFileInfo | null = null + constructor(readonly cacheDir: string) { + } + get file() { - return this.setupPath + return this._file } - get packagePath() { - return this._packagePath + get packageFile() { + return this._packageFile } - getDownloadedFile(versionInfo: UpdateInfo, fileInfo: ResolvedUpdateFileInfo): string | null { - if (this.setupPath == null) { - return null + async validateDownloadedPath(updateFile: string, versionInfo: UpdateInfo, fileInfo: ResolvedUpdateFileInfo, logger: Logger): Promise { + if (this.versionInfo != null && this.file === updateFile) { + // update has already been downloaded from this running instance + // check here only existence, not checksum + return isEqual(this.versionInfo, versionInfo) && isEqual(this.fileInfo, fileInfo) && (await pathExists(updateFile)) } - return isEqual(this.versionInfo, versionInfo) && isEqual(this.fileInfo, fileInfo) ? this.setupPath : null - } + // update has already been downloaded from some previous app launch + if (await DownloadedUpdateHelper.isUpdateValid(updateFile, fileInfo, logger)) { + logger.info(`Update has already been downloaded ${updateFile}).`) + return true + } - setDownloadedFile(file: string, packagePath: string | null, versionInfo: UpdateInfo, fileInfo: ResolvedUpdateFileInfo) { - this.setupPath = file - this._packagePath = packagePath + return false + } + setDownloadedFile(downloadedFile: string, packageFile: string | null, versionInfo: UpdateInfo, fileInfo: ResolvedUpdateFileInfo) { + this._file = downloadedFile + this._packageFile = packageFile this.versionInfo = versionInfo this.fileInfo = fileInfo } clear() { - this.setupPath = null - this._packagePath = null - + this._file = null this.versionInfo = null this.fileInfo = null } + + private static async isUpdateValid(updateFile: string, fileInfo: ResolvedUpdateFileInfo, logger: Logger): Promise { + if (!(await pathExists(updateFile))) { + logger.info("No cached update available") + return false + } + + const sha512 = await hashFile(updateFile) + if (fileInfo.info.sha512 !== sha512) { + logger.warn(`Sha512 checksum doesn't match the latest available update. New update must be downloaded. Cached: ${sha512}, expected: ${fileInfo.info.sha512}`) + return false + } + return true + } +} + +function hashFile(file: string, algorithm: string = "sha512", encoding: "base64" | "hex" = "base64", options?: any) { + return new Promise((resolve, reject) => { + const hash = createHash(algorithm) + hash + .on("error", reject) + .setEncoding(encoding) + + createReadStream(file, {...options, highWaterMark: 1024 * 1024 /* better to use more memory but hash faster */}) + .on("error", reject) + .on("end", () => { + hash.end() + resolve(hash.read() as string) + }) + .pipe(hash, {end: false}) + }) } \ No newline at end of file diff --git a/packages/electron-updater/src/NsisUpdater.ts b/packages/electron-updater/src/NsisUpdater.ts index f311bc28f60..1529ea7899b 100644 --- a/packages/electron-updater/src/NsisUpdater.ts +++ b/packages/electron-updater/src/NsisUpdater.ts @@ -6,8 +6,9 @@ import "source-map-support/register" import { BaseUpdater } from "./BaseUpdater" import { FileWithEmbeddedBlockMapDifferentialDownloader } from "./differentialDownloader/FileWithEmbeddedBlockMapDifferentialDownloader" import { GenericDifferentialDownloader } from "./differentialDownloader/GenericDifferentialDownloader" -import { newUrlFromBase, ResolvedUpdateFileInfo, UPDATE_DOWNLOADED } from "./main" +import { newUrlFromBase, ResolvedUpdateFileInfo } from "./main" import { findFile, Provider } from "./Provider" +import { unlink } from "fs-extra-p" import { verifySignature } from "./windowsExecutableCodeSignatureVerifier" export class NsisUpdater extends BaseUpdater { @@ -27,44 +28,48 @@ export class NsisUpdater extends BaseUpdater { sha512: fileInfo.info.sha512, } - let packagePath: string | null = this.downloadedUpdateHelper.packagePath - - let installerPath = this.downloadedUpdateHelper.getDownloadedFile(updateInfo, fileInfo) - if (installerPath != null) { - return packagePath == null ? [installerPath] : [installerPath, packagePath] - } - - await this.executeDownload(downloadOptions, fileInfo, async (tempDir, destinationFile, removeTempDirIfAny) => { - installerPath = destinationFile - if (await this.differentialDownloadInstaller(fileInfo, path.join(this.app.getPath("userData"), "installer.exe"), installerPath, requestHeaders, provider)) { - await this.httpExecutor.download(fileInfo.url.href, installerPath, downloadOptions) - } + return await this.executeDownload({ + fileExtension: "exe", + downloadOptions, + fileInfo, + updateInfo, + task: async (destinationFile, packageFile, removeTempDirIfAny) => { + if (await this.differentialDownloadInstaller(fileInfo, destinationFile, requestHeaders, provider)) { + await this.httpExecutor.download(fileInfo.url.href, destinationFile, downloadOptions) + } - const signatureVerificationStatus = await this.verifySignature(installerPath) - if (signatureVerificationStatus != null) { - await removeTempDirIfAny() - // noinspection ThrowInsideFinallyBlockJS - throw newError(`New version ${this.updateInfo!.version} is not signed by the application owner: ${signatureVerificationStatus}`, "ERR_UPDATER_INVALID_SIGNATURE") - } + const signatureVerificationStatus = await this.verifySignature(destinationFile) + if (signatureVerificationStatus != null) { + await removeTempDirIfAny() + // noinspection ThrowInsideFinallyBlockJS + throw newError(`New version ${updateInfo!.version} is not signed by the application owner: ${signatureVerificationStatus}`, "ERR_UPDATER_INVALID_SIGNATURE") + } - const packageInfo = fileInfo.packageInfo - if (packageInfo != null) { - packagePath = path.join(tempDir, `package-${updateInfo.version}${path.extname(packageInfo.path) || ".7z"}`) - if (await this.differentialDownloadWebPackage(packageInfo, packagePath, provider)) { - await this.httpExecutor.download(packageInfo.path, packagePath!!, { - skipDirCreation: true, - headers: requestHeaders, - cancellationToken, - sha512: packageInfo.sha512, - }) + const packageInfo = fileInfo.packageInfo + if (packageInfo != null && packageFile != null) { + if (await this.differentialDownloadWebPackage(packageInfo, packageFile, provider)) { + try { + await this.httpExecutor.download(packageInfo.path, packageFile, { + skipDirCreation: true, + headers: requestHeaders, + cancellationToken, + sha512: packageInfo.sha512, + }) + } + catch (e) { + try { + await unlink(packageFile) + } + catch (ignored) { + // ignore + } + + throw e + } + } } - } + }, }) - - this.downloadedUpdateHelper.setDownloadedFile(installerPath!!, packagePath, updateInfo, fileInfo) - this.addQuitHandler() - this.emit(UPDATE_DOWNLOADED, this.updateInfo) - return packagePath == null ? [installerPath!!] : [installerPath!!, packagePath] } // $certificateInfo = (Get-AuthenticodeSignature 'xxx\yyy.exe' @@ -98,7 +103,7 @@ export class NsisUpdater extends BaseUpdater { args.push("--force-run") } - const packagePath = this.downloadedUpdateHelper.packagePath + const packagePath = this.downloadedUpdateHelper.packageFile if (packagePath != null) { // only = form is supported args.push(`--package-file=${packagePath}`) @@ -134,7 +139,7 @@ export class NsisUpdater extends BaseUpdater { return true } - private async differentialDownloadInstaller(fileInfo: ResolvedUpdateFileInfo, oldFile: string, installerPath: string, requestHeaders: OutgoingHttpHeaders, provider: Provider) { + private async differentialDownloadInstaller(fileInfo: ResolvedUpdateFileInfo, installerPath: string, requestHeaders: OutgoingHttpHeaders, provider: Provider) { if (process.env.__NSIS_DIFFERENTIAL_UPDATE__ == null) { return true } @@ -143,7 +148,7 @@ export class NsisUpdater extends BaseUpdater { const blockMapData = JSON.parse((await provider.httpRequest(newUrlFromBase(`${fileInfo.url.pathname}.blockMap.json`, fileInfo.url)))!!) await new GenericDifferentialDownloader(fileInfo.info, this.httpExecutor, { newUrl: fileInfo.url.href, - oldFile, + oldFile: path.join(this.app.getPath("userData"), "installer.exe"), logger: this._logger, newFile: installerPath, useMultipleRangeRequest: provider.useMultipleRangeRequest, diff --git a/test/out/updater/__snapshots__/differentialUpdateTest.js.snap b/test/out/updater/__snapshots__/differentialUpdateTest.js.snap index ca3678f3cf3..ce7a9f5f572 100644 --- a/test/out/updater/__snapshots__/differentialUpdateTest.js.snap +++ b/test/out/updater/__snapshots__/differentialUpdateTest.js.snap @@ -84,7 +84,7 @@ Object { exports[`AppImage 4`] = ` Array [ - "TestApp-1.0.1-x86_64.AppImage", + "installer-1.0.1.AppImage", ] `; @@ -382,7 +382,7 @@ Object { exports[`web installer 4`] = ` Array [ - "Test App ßW Web Setup 1.0.1.exe", - "package-1.0.1.7z", + "installer-1.0.1.exe", + "package-1.0.1..7z", ] `; diff --git a/test/out/updater/__snapshots__/nsisUpdaterTest.js.snap b/test/out/updater/__snapshots__/nsisUpdaterTest.js.snap index ae1941a27be..9c4b038561f 100644 --- a/test/out/updater/__snapshots__/nsisUpdaterTest.js.snap +++ b/test/out/updater/__snapshots__/nsisUpdaterTest.js.snap @@ -407,6 +407,26 @@ Array [ ] `; +exports[`test download and install 1`] = ` +Object { + "files": Array [ + Object { + "sha512": "Dj51I0q8aPQ3ioaz9LMqGYujAYRbDNblAQbodDRXAMxmY6hsHqEl3F6SvhfJj5oPhcqdX1ldsgEvfMNXGUXBIw==", + "url": "TestApp Setup 1.1.0.exe", + }, + ], + "version": "1.1.0", +} +`; + +exports[`test download and install 2`] = ` +Array [ + "checking-for-update", + "update-available", + "update-downloaded", +] +`; + exports[`test error 1`] = `"ERR_INVALID_ARG_TYPE"`; exports[`test error 2`] = ` diff --git a/test/src/updater/differentialUpdateTest.ts b/test/src/updater/differentialUpdateTest.ts index b16587cec01..2fb52808971 100644 --- a/test/src/updater/differentialUpdateTest.ts +++ b/test/src/updater/differentialUpdateTest.ts @@ -292,7 +292,7 @@ async function testBlockMap(oldDir: string, newDir: string, updaterClass: any) { const port = 8000 + updaterClass.name.charCodeAt(0) // noinspection SpellCheckingInspection - const httpServerProcess = doSpawn(path.join(await getBinFromGithub("ran", "0.1.3", "imfA3LtT6umMM0BuQ29MgO3CJ9uleN5zRBi3sXzcTbMOeYZ6SQeN7eKr3kXZikKnVOIwbH+DDO43wkiR/qTdkg=="), process.platform, "ran"), [`-root=${newDir}`, `-port=${port}`]) + const httpServerProcess = doSpawn(path.join(await getBinFromGithub("ran", "0.1.3", "imfA3LtT6umMM0BuQ29MgO3CJ9uleN5zRBi3sXzcTbMOeYZ6SQeN7eKr3kXZikKnVOIwbH+DDO43wkiR/qTdkg=="), process.platform, "ran"), [`-root=${newDir}`, `-port=${port}`, "-gzip=false"]) { (process as any).resourcesPath = path.join(oldDir, "win-unpacked", "resources") } diff --git a/test/src/updater/nsisUpdaterTest.ts b/test/src/updater/nsisUpdaterTest.ts index 5fc1977ecaf..56d28e6e5d5 100644 --- a/test/src/updater/nsisUpdaterTest.ts +++ b/test/src/updater/nsisUpdaterTest.ts @@ -357,4 +357,33 @@ test.skip("cancel download with progress", async () => { await assertThat(downloadPromise).throws() expect(downloadPromise.isRejected()).toBe(true) expect(cancelled).toBe(true) +}) + +test.ifAll("test download and install", async () => { + const updater = new NsisUpdater() + updater.updateConfigPath = await writeUpdateConfig({ + provider: "generic", + url: "https://develar.s3.amazonaws.com/test", + }) + tuneNsisUpdater(updater) + + await validateDownload(updater) + + const actualEvents = trackEvents(updater) + expect(actualEvents).toMatchObject([]) + // await updater.quitAndInstall(true, false) +}) + +test.ifAll("test downloaded installer", async () => { + const updater = new NsisUpdater() + updater.updateConfigPath = await writeUpdateConfig({ + provider: "generic", + url: "https://develar.s3.amazonaws.com/test", + }) + tuneNsisUpdater(updater) + + const actualEvents = trackEvents(updater) + + expect(actualEvents).toMatchObject([]) + // await updater.quitAndInstall(true, false) }) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9898c0b3ffd..d1da438c516 100644 --- a/yarn.lock +++ b/yarn.lock @@ -112,9 +112,9 @@ version "1.3.29" resolved "https://registry.yarnpkg.com/@types/ini/-/ini-1.3.29.tgz#1325e981e047d40d13ce0359b821475b97741d2f" -"@types/jest@^22.1.2": - version "22.1.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.1.2.tgz#813a79ec98221633845627636dbc606f31220dbc" +"@types/jest@^22.1.3": + version "22.1.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.1.3.tgz#25da391935e6fac537551456f077ce03144ec168" "@types/js-yaml@^3.10.1": version "3.10.1" @@ -484,9 +484,9 @@ atob@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" -aws-sdk@^2.196.0: - version "2.196.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.196.0.tgz#0fabf9b1d22997c59d77a025c6bd4666924f147c" +aws-sdk@^2.197.0: + version "2.197.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.197.0.tgz#986e3749f4d1944c6253d7824aa8035efb1db0e4" dependencies: buffer "4.9.1" events "^1.1.1" @@ -2113,6 +2113,13 @@ fs-extra-p@^4.4.4, fs-extra-p@^4.5.0: bluebird-lst "^1.0.5" fs-extra "^5.0.0" +fs-extra-p@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/fs-extra-p/-/fs-extra-p-4.5.2.tgz#0a22aba489284d17f375d5dc5139aa777fe2df51" + dependencies: + bluebird-lst "^1.0.5" + fs-extra "^5.0.0" + fs-extra@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -2336,9 +2343,9 @@ globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" -globby@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.0.tgz#e6f8340ead9a52fa417ec0e75ae664ae0026f5c6" +globby@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" dependencies: array-union "^1.0.1" dir-glob "^2.0.0"