Skip to content
Permalink
Browse files

feat(electron-updater): decouple Electron API to support Proton and o…

…ther frameworks
  • Loading branch information...
develar committed Nov 12, 2018
1 parent 277a3ad commit 9422251ec1535a9fded552940d021eb2ba4ffc4e
@@ -2,12 +2,17 @@

### BREAKING CHANGES

Requires Electron 3 or later.
* Requires Electron 3 or later.
* Cache directory changed, so, full download will be performed on update instead of differential.

### Bug Fixes

* use cache dir for electron-updater cache data

### Features

* decouple Electron API to support Proton and other frameworks

## 3.2.3

### Bug Fixes
@@ -0,0 +1,45 @@
import * as path from "path"

export interface AppAdapter {
readonly version: string
readonly name: string

readonly isPackaged: boolean

/**
* Path to update metadata file.
*/
readonly appUpdateConfigPath: string

/**
* Path to user data directory.
*/
readonly userDataPath: string

/**
* Path to cache directory.
*/
readonly baseCachePath: string

whenReady(): Promise<void>

quit(): void

onQuit(handler: (exitCode: number) => void): void
}

export function getAppCacheDir() {
const homedir = require("os").homedir()
// https://github.com/electron/electron/issues/1404#issuecomment-194391247
let result: string
if (process.platform === "win32") {
result = process.env.LOCALAPPDATA || path.join(homedir, "AppData", "Local")
}
else if (process.platform === "darwin") {
result = path.join(homedir, "Library", "Application Support", "Caches")
}
else {
result = process.env.XDG_CACHE_HOME || path.join(homedir, ".cache")
}
return result
}
@@ -74,7 +74,7 @@ export class AppImageUpdater extends BaseUpdater {
})
}

protected async doInstall(installerPath: string, isSilent: boolean, isRunAfter: boolean): Promise<boolean> {
protected doInstall(installerPath: string, isSilent: boolean, isRunAfter: boolean): boolean {
const appImageFile = process.env.APPIMAGE!!
if (appImageFile == null) {
throw newError("APPIMAGE env is not defined", "ERR_UPDATER_OLD_FILE_NOT_FOUND")
@@ -1,7 +1,6 @@
import { AllPublishOptions, asArray, CancellationToken, newError, PublishConfiguration, UpdateInfo, UUID, DownloadOptions, CancellationError } from "builder-util-runtime"
import { randomBytes } from "crypto"
import { Notification } from "electron"
import isDev from "electron-is-dev"
import { EventEmitter } from "events"
import { ensureDir, outputFile, readFile, rename, unlink } from "fs-extra-p"
import { OutgoingHttpHeaders } from "http"
@@ -10,7 +9,9 @@ import { Lazy } from "lazy-val"
import * as path from "path"
import { eq as isVersionsEqual, gt as isVersionGreaterThan, parse as parseVersion, prerelease as getVersionPreleaseComponents, SemVer } from "semver"
import "source-map-support/register"
import { AppAdapter } from "./AppAdapter"
import { createTempUpdateFile, DownloadedUpdateHelper } from "./DownloadedUpdateHelper"
import { ElectronAppAdapter } from "./ElectronAppAdapter"
import { ElectronHttpExecutor, getNetSession } from "./electronHttpExecutor"
import { GenericProvider } from "./providers/GenericProvider"
import { DOWNLOAD_PROGRESS, Logger, Provider, ResolvedUpdateFileInfo, UPDATE_DOWNLOADED, UpdateCheckResult, UpdateDownloadedEvent, UpdaterSignal } from "./main"
@@ -96,8 +97,7 @@ export abstract class AppUpdater extends EventEmitter {

protected _logger: Logger = console

// noinspection JSUnusedGlobalSymbols
// noinspection JSMethodCanBeStatic
// noinspection JSMethodCanBeStatic,JSUnusedGlobalSymbols
get netSession(): Session {
return getNetSession()
}
@@ -144,39 +144,31 @@ export abstract class AppUpdater extends EventEmitter {
private readonly untilAppReady: Promise<any>
private checkForUpdatesPromise: Promise<UpdateCheckResult> | null = null

protected readonly app: Electron.App
protected readonly app: AppAdapter

protected updateInfoAndProvider: UpdateInfoAndProvider | null = null

/** @internal */
readonly httpExecutor: ElectronHttpExecutor

protected constructor(options: AllPublishOptions | null | undefined, app?: Electron.App) {
protected constructor(options: AllPublishOptions | null | undefined, app?: AppAdapter) {
super()

this.on("error", (error: Error) => {
this._logger.error(`Error: ${error.stack || error.message}`)
})

if (app != null || (global as any).__test_app != null) {
this.app = app || (global as any).__test_app
this.untilAppReady = Promise.resolve()
this.httpExecutor = null as any
if (app == null) {
this.app = new ElectronAppAdapter()
this.httpExecutor = new ElectronHttpExecutor((authInfo, callback) => this.emit("login", authInfo, callback))
}
else {
this.app = require("electron").app
this.httpExecutor = new ElectronHttpExecutor((authInfo, callback) => this.emit("login", authInfo, callback))
this.untilAppReady = new Promise(resolve => {
if (this.app.isReady()) {
resolve()
}
else {
this.app.on("ready", resolve)
}
})
this.app = app
this.httpExecutor = null as any
}
this.untilAppReady = Promise.resolve()

const currentVersionString = this.app.getVersion()
const currentVersionString = this.app.version
const currentVersion = parseVersion(currentVersionString)
if (currentVersion == null) {
throw newError(`App version is not a valid semver version: "${currentVersionString}"`, "ERR_UPDATER_INVALID_VERSION")
@@ -233,7 +225,7 @@ export abstract class AppUpdater extends EventEmitter {
}

checkForUpdatesAndNotify(): Promise<UpdateCheckResult | null> {
if (isDev) {
if (this.app.isPackaged) {
return Promise.resolve(null)
}

@@ -253,7 +245,7 @@ export abstract class AppUpdater extends EventEmitter {
.then(() => {
new Notification({
title: "A new update is ready to install",
body: `${this.app.getName()} version ${it.updateInfo.version} has been downloaded and will be automatically installed on exit`
body: `${this.app.name} version ${it.updateInfo.version} has been downloaded and will be automatically installed on exit`
}).show()
})
})
@@ -457,7 +449,7 @@ export abstract class AppUpdater extends EventEmitter {

private async loadUpdateConfig() {
if (this._appUpdateConfigPath == null) {
this._appUpdateConfigPath = isDev ? path.join(this.app.getAppPath(), "dev-app-update.yml") : path.join(process.resourcesPath!!, "app-update.yml")
this._appUpdateConfigPath = this.app.appUpdateConfigPath
}
return safeLoad(await readFile(this._appUpdateConfigPath, "utf-8"))
}
@@ -475,7 +467,7 @@ export abstract class AppUpdater extends EventEmitter {
}

private async getOrCreateStagingUserId(): Promise<string> {
const file = path.join(this._testOnlyOptions == null ? this.app.getPath("userData") : this._testOnlyOptions.cacheDir, ".updaterId")
const file = path.join(this.app.userDataPath, ".updaterId")
try {
const id = await readFile(file, "utf-8")
if (UUID.check(id)) {
@@ -533,7 +525,7 @@ export abstract class AppUpdater extends EventEmitter {
if (dirName == null) {
logger.error("updaterCacheDirName is not specified in app-update.yml Was app build using at least electron-builder 20.34.0?")
}
const cacheDir = this._testOnlyOptions == null ? getCacheDir(this.app, dirName || this.app.getName()) : this._testOnlyOptions.cacheDir
const cacheDir = path.join(this.app.baseCachePath, dirName || this.app.name)
if (logger.debug != null) {
logger.debug(`updater cache dir: ${cacheDir}`)
}
@@ -671,26 +663,9 @@ export interface DownloadExecutorTask {
readonly done?: (event: UpdateDownloadedEvent) => Promise<any>
}

function getCacheDir(app: Electron.App, dirName: string) {
const homedir = require("os").homedir()
// https://github.com/electron/electron/issues/1404#issuecomment-194391247
let baseDir: string
if (process.platform === "win32") {
baseDir = process.env.LOCALAPPDATA || app.getPath("userData")
}
else if (process.platform === "darwin") {
baseDir = path.join(homedir, "Library", "Application Support", "Caches")
}
else {
baseDir = process.env.XDG_CACHE_HOME || path.join(homedir, ".cache")
}
return path.join(baseDir, dirName)
}

/** @private */
export interface TestOnlyUpdaterOptions {
platform: ProviderPlatform
cacheDir: string

isUseDifferentialDownload?: boolean
}
@@ -1,11 +1,12 @@
import { AllPublishOptions } from "builder-util-runtime"
import { AppAdapter } from "./AppAdapter"
import { AppUpdater, DownloadExecutorTask } from "./AppUpdater"

export abstract class BaseUpdater extends AppUpdater {
protected quitAndInstallCalled = false
private quitHandlerAdded = false

protected constructor(options?: AllPublishOptions | null, app?: any) {
protected constructor(options?: AllPublishOptions | null, app?: AppAdapter) {
super(options, app)
}

@@ -14,9 +15,7 @@ export abstract class BaseUpdater extends AppUpdater {
const isInstalled = await this.install(isSilent, isSilent ? isForceRunAfter : true)
if (isInstalled) {
setImmediate(() => {
if (this.app.quit !== undefined) {
this.app.quit()
}
this.app.quit()
})
}
else {
@@ -34,9 +33,11 @@ export abstract class BaseUpdater extends AppUpdater {
})
}

protected abstract doInstall(installerPath: string, isSilent: boolean, isRunAfter: boolean): Promise<boolean>
// must be sync
protected abstract doInstall(installerPath: string, isSilent: boolean, isRunAfter: boolean): boolean

protected async install(isSilent: boolean, isRunAfter: boolean): Promise<boolean> {
// must be sync (because quit even handler is not async)
protected install(isSilent: boolean, isRunAfter: boolean): boolean {
if (this.quitAndInstallCalled) {
this._logger.warn("install call ignored: quitAndInstallCalled is set to true")
return false
@@ -53,7 +54,7 @@ export abstract class BaseUpdater extends AppUpdater {

try {
this._logger.info(`Install: isSilent: ${isSilent}, isRunAfter: ${isRunAfter}`)
return await this.doInstall(installerPath, isSilent, isRunAfter)
return this.doInstall(installerPath, isSilent, isRunAfter)
}
catch (e) {
this.dispatchError(e)
@@ -68,14 +69,19 @@ export abstract class BaseUpdater extends AppUpdater {

this.quitHandlerAdded = true

this.app.once("quit", async () => {
if (!this.quitAndInstallCalled) {
this._logger.info("Auto install update on quit")
await this.install(true, false)
}
else {
this.app.onQuit(exitCode => {
if (this.quitAndInstallCalled) {
this._logger.info("Update installer has already been triggered. Quitting application.")
return
}

if (exitCode !== 0) {
this._logger.info(`Update will be not installed on quit because application is quitting with exit code ${exitCode}`)
return
}

this._logger.info("Auto install update on quit")
this.install(true, false)
})
}
}
@@ -0,0 +1,43 @@
import * as path from "path"
import { AppAdapter, getAppCacheDir } from "./AppAdapter"

export class ElectronAppAdapter implements AppAdapter {
constructor(private readonly app = require("electron").app) {
}

whenReady(): Promise<void> {
return this.app.whenReady()
}

get version(): string {
return this.app.getVersion()
}

get name(): string {
return this.app.getName()
}

get isPackaged(): boolean {
return this.app.isPackaged === true
}

get appUpdateConfigPath(): string {
return this.isPackaged ? path.join(process.resourcesPath!!, "app-update.yml") : path.join(this.app.getAppPath(), "dev-app-update.yml")
}

get userDataPath(): string {
return this.app.getPath("userData")
}

get baseCachePath(): string {
return getAppCacheDir()
}

quit(): void {
this.app.quit()
}

onQuit(handler: (exitCode: number) => void): void {
this.app.once("quit", (event, exitCode) => handler(exitCode))
}
}
@@ -2,6 +2,7 @@ import { AllPublishOptions, newError, safeStringifyJson } from "builder-util-run
import { createReadStream, stat } from "fs-extra-p"
import { createServer, IncomingMessage, ServerResponse } from "http"
import { AddressInfo } from "net"
import { AppAdapter } from "./AppAdapter"
import { AppUpdater, DownloadUpdateOptions } from "./AppUpdater"
import { UpdateDownloadedEvent } from "./main"
import { findFile } from "./providers/Provider"
@@ -12,8 +13,8 @@ export class MacUpdater extends AppUpdater {

private updateInfoForPendingUpdateDownloadedEvent: UpdateDownloadedEvent | null = null

constructor(options?: AllPublishOptions) {
super(options)
constructor(options?: AllPublishOptions, app?: AppAdapter) {
super(options, app)

this.nativeUpdater.on("error", it => {
this._logger.warn(it)

0 comments on commit 9422251

Please sign in to comment.
You can’t perform that action at this time.