Skip to content

Commit

Permalink
WIP: Staged rollouts electron-userland#1639
Browse files Browse the repository at this point in the history
  • Loading branch information
develar committed Jun 12, 2017
1 parent a7f1a1c commit d3526ee
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 110 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"ajv": "^5.1.5",
"ajv-keywords": "^2.1.0",
"archiver": "^1.3.0",
"aws-sdk": "^2.65.0",
"aws-sdk": "^2.67.0",
"bluebird-lst": "^1.0.2",
"chalk": "^1.1.3",
"chromium-pickle-js": "^0.2.0",
Expand Down
10 changes: 8 additions & 2 deletions packages/electron-builder-http/src/bintray.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CancellationToken } from "./CancellationToken"
import { configureRequestOptions, HttpExecutor } from "./httpExecutor"
import { configureRequestOptions, HttpExecutor, RequestHeaders } from "./httpExecutor"
import { BintrayOptions } from "./publishOptions"

export interface Version {
Expand All @@ -25,6 +25,12 @@ export class BintrayClient {
readonly user: string
readonly packageName: string

private requestHeaders: RequestHeaders | null

setRequestHeaders(value: RequestHeaders | null) {
this.requestHeaders = value
}

constructor(options: BintrayOptions, private readonly httpExecutor: HttpExecutor<any>, private readonly cancellationToken: CancellationToken, apiKey?: string | null) {
if (options.owner == null) {
throw new Error("owner is not specified")
Expand All @@ -42,7 +48,7 @@ export class BintrayClient {
}

private bintrayRequest<T>(path: string, auth: string | null, data: {[name: string]: any; } | null = null, cancellationToken: CancellationToken, method?: "GET" | "DELETE" | "PUT"): Promise<T> {
return this.httpExecutor.request<T>(configureRequestOptions({hostname: "api.bintray.com", path: path}, auth, method), cancellationToken, data)
return this.httpExecutor.request<T>(configureRequestOptions({hostname: "api.bintray.com", path: path, headers: this.requestHeaders || undefined}, auth, method), cancellationToken, data)
}

getVersion(version: string): Promise<Version> {
Expand Down
2 changes: 1 addition & 1 deletion packages/electron-publisher-s3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
],
"dependencies": {
"fs-extra-p": "^4.3.0",
"aws-sdk": "^2.65.0",
"aws-sdk": "^2.67.0",
"mime": "^1.3.6",
"electron-publish": "~0.0.0-semantic-release",
"electron-builder-util": "~0.0.0-semantic-release"
Expand Down
3 changes: 2 additions & 1 deletion packages/electron-updater/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"electron-builder-http": "~0.0.0-semantic-release",
"electron-is-dev": "^0.1.2",
"xelement": "^1.0.16",
"debug": "^2.6.8"
"debug": "^2.6.8",
"uuid-1345": "^0.99.6"
},
"typings": "./out/electron-updater.d.ts"
}
95 changes: 69 additions & 26 deletions packages/electron-updater/src/AppUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import BluebirdPromise from "bluebird-lst"
import { randomBytes } from "crypto"
import { RequestHeaders } from "electron-builder-http"
import { CancellationToken } from "electron-builder-http/out/CancellationToken"
import { BintrayOptions, GenericServerOptions, GithubOptions, PublishConfiguration, S3Options, s3Url, VersionInfo } from "electron-builder-http/out/publishOptions"
import { EventEmitter } from "events"
import { readFile } from "fs-extra-p"
import { readFile, writeFile } from "fs-extra-p"
import { safeLoad } from "js-yaml"
import * as path from "path"
import { eq as isVersionsEqual, gt as isVersionGreaterThan, prerelease as getVersionPreleaseComponents, valid as parseVersion } from "semver"
import "source-map-support/register"
import * as UUID from "uuid-1345"
import { BintrayProvider } from "./BintrayProvider"
import { ElectronHttpExecutor } from "./electronHttpExecutor"
import { GenericProvider } from "./GenericProvider"
Expand Down Expand Up @@ -39,11 +41,19 @@ export abstract class AppUpdater extends EventEmitter {
*/
requestHeaders: RequestHeaders | null

protected _logger: Logger = console

/**
* The logger. You can pass [electron-log](https://github.com/megahertz/electron-log), [winston](https://github.com/winstonjs/winston) or another logger with the following interface: `{ info(), warn(), error() }`.
* Set it to `null` if you would like to disable a logging feature.
*/
logger: Logger | null = (<any>global).__test_app ? null : console
get logger(): Logger | null {
return this._logger
}

set logger(value: Logger | null) {
this._logger = value == null ? new NoOpLogger() : value
}

/**
* For type safety you can use signals, e.g. `autoUpdater.signals.updateDownloaded(() => {})` instead of `autoUpdater.on('update-available', () => {})`
Expand All @@ -61,6 +71,17 @@ export abstract class AppUpdater extends EventEmitter {

private clientPromise: Promise<Provider<any>> | null

private _stagingUserIdPromise: Promise<string> | null

protected get stagingUserIdPromise(): Promise<string> {
let result = this._stagingUserIdPromise
if (result == null) {
result = this.getOrCreateStagedUserId()
this._stagingUserIdPromise = result
}
return result
}

private readonly untilAppReady: Promise<boolean>
private checkForUpdatesPromise: Promise<UpdateCheckResult> | null

Expand All @@ -77,9 +98,7 @@ export abstract class AppUpdater extends EventEmitter {
super()

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

if (app != null || (<any>global).__test_app != null) {
Expand All @@ -91,15 +110,11 @@ export abstract class AppUpdater extends EventEmitter {
this.httpExecutor = new ElectronHttpExecutor((authInfo, callback) => this.emit("login", authInfo, callback))
this.untilAppReady = new BluebirdPromise(resolve => {
if (this.app.isReady()) {
if (this.logger != null) {
this.logger.info("App is ready")
}
this._logger.info("App is ready")
resolve()
}
else {
if (this.logger != null) {
this.logger.info("Wait for app ready")
}
this._logger.info("Wait for app ready")
this.app.on("ready", resolve)
}
})
Expand Down Expand Up @@ -160,11 +175,7 @@ export abstract class AppUpdater extends EventEmitter {
private async _checkForUpdates(): Promise<UpdateCheckResult> {
try {
await this.untilAppReady

if (this.logger != null) {
this.logger.info("Checking for update")
}

this._logger.info("Checking for update")
this.emit("checking-for-update")
return await this.doCheckForUpdates()
}
Expand All @@ -180,7 +191,8 @@ export abstract class AppUpdater extends EventEmitter {
}

const client = await this.clientPromise
client.setRequestHeaders(this.requestHeaders)
const stagingUserId = await this.stagingUserIdPromise
client.setRequestHeaders(Object.assign({"X-User-Staging-Id": stagingUserId}, this.requestHeaders))
const versionInfo = await client.getLatestVersion()

const latestVersion = parseVersion(versionInfo.version)
Expand All @@ -190,9 +202,7 @@ export abstract class AppUpdater extends EventEmitter {

if (this.allowDowngrade && !hasPrereleaseComponents(latestVersion) ? isVersionsEqual(latestVersion, this.currentVersion) : !isVersionGreaterThan(latestVersion, this.currentVersion)) {
this.updateAvailable = false
if (this.logger != null) {
this.logger.info(`Update for version ${this.currentVersion} is not available (latest version: ${versionInfo.version}, downgrade is ${this.allowDowngrade ? "allowed" : "disallowed"}.`)
}
this._logger.info(`Update for version ${this.currentVersion} is not available (latest version: ${versionInfo.version}, downgrade is ${this.allowDowngrade ? "allowed" : "disallowed"}.`)
this.emit("update-not-available", versionInfo)
return {
versionInfo: versionInfo,
Expand All @@ -218,9 +228,7 @@ export abstract class AppUpdater extends EventEmitter {
}

protected onUpdateAvailable(versionInfo: VersionInfo, fileInfo: FileInfo) {
if (this.logger != null) {
this.logger.info(`Found version ${versionInfo.version} (url: ${fileInfo.url})`)
}
this._logger.info(`Found version ${versionInfo.version} (url: ${fileInfo.url})`)
this.emit("update-available", versionInfo)
}

Expand All @@ -238,9 +246,7 @@ export abstract class AppUpdater extends EventEmitter {
throw error
}

if (this.logger != null) {
this.logger.info(`Downloading update from ${fileInfo.url}`)
}
this._logger.info(`Downloading update from ${fileInfo.url}`)

try {
return await this.doDownloadUpdate(versionInfo, fileInfo, cancellationToken)
Expand Down Expand Up @@ -320,9 +326,46 @@ export abstract class AppUpdater extends EventEmitter {
throw new Error(`Unsupported provider: ${provider}`)
}
}

private async getOrCreateStagedUserId(): Promise<string> {
const file = path.join(this.app.getPath("userData"), ".updaterId")
try {
const id = await readFile(file, "utf-8")
if (UUID.check(id)) {
return id
}
else {
this._logger.warn(`Staging user id file exists, but content was invalid: ${id}`)
}
}
catch (e) {
if (e.code !== "ENOENT") {
this._logger.warn(`Couldn't read staging user ID, creating a blank one: ${e}`)
}
}

const id = UUID.v5({name: randomBytes(4096), namespace: UUID.namespace.oid})
this._logger.info(`Generated new staging user ID: ${id}`)
try {
await writeFile(file, id)
}
catch (e) {
this._logger.warn(`Couldn't write out staging user ID: ${e}`)
}
return id
}
}

function hasPrereleaseComponents(version: string) {
const versionPrereleaseComponent = getVersionPreleaseComponents(version)
return versionPrereleaseComponent != null && versionPrereleaseComponent.length > 0
}

/** @private */
export class NoOpLogger implements Logger {
info(message?: any) {}

warn(message?: any) {}

error(message?: any) {}
}
5 changes: 5 additions & 0 deletions packages/electron-updater/src/BintrayProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { FileInfo, Provider } from "./main"
export class BintrayProvider extends Provider<VersionInfo> {
private client: BintrayClient

setRequestHeaders(value: any): void {
super.setRequestHeaders(value)
this.client.setRequestHeaders(value)
}

constructor(configuration: BintrayOptions, httpExecutor: HttpExecutor<any>) {
super()

Expand Down
12 changes: 3 additions & 9 deletions packages/electron-updater/src/MacUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,19 @@ export class MacUpdater extends AppUpdater {
super(options)

this.nativeUpdater.on("error", it => {
if (this.logger != null) {
this.logger.warn(it)
}
this._logger.warn(it)
this.emit("error", it)
})
this.nativeUpdater.on("update-downloaded", () => {
if (this.logger != null) {
this.logger.info(`New version ${this.versionInfo!.version} has been downloaded`)
}
this._logger.info(`New version ${this.versionInfo!.version} has been downloaded`)
this.emit(UPDATE_DOWNLOADED, this.versionInfo)
})
}

protected doDownloadUpdate(versionInfo: VersionInfo, fileInfo: FileInfo, cancellationToken: CancellationToken) {
const server = createServer()
server.on("close", () => {
if (this.logger != null) {
this.logger.info(`Proxy server for native Squirrel.Mac is closed (was started to download ${fileInfo.url})`)
}
this._logger.info(`Proxy server for native Squirrel.Mac is closed (was started to download ${fileInfo.url})`)
})

function getServerUrl() {
Expand Down
23 changes: 5 additions & 18 deletions packages/electron-updater/src/NsisUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export class NsisUpdater extends AppUpdater {
downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it)
}

const logger = this.logger
const tempDir = await mkdtemp(`${path.join(tmpdir(), "up")}-`)
const tempFile = path.join(tempDir, fileInfo.name)
try {
Expand All @@ -48,9 +47,7 @@ export class NsisUpdater extends AppUpdater {

if (e instanceof CancellationError) {
this.emit("update-cancelled", this.versionInfo)
if (logger != null) {
logger.info("Cancelled")
}
this._logger.info("Cancelled")
}
throw e
}
Expand All @@ -66,10 +63,7 @@ export class NsisUpdater extends AppUpdater {
}
}

if (logger != null) {
logger.info(`New version ${this.versionInfo!.version} has been downloaded to ${tempFile}`)
}

this._logger.info(`New version ${this.versionInfo!.version} has been downloaded to ${tempFile}`)
this.setupPath = tempFile
this.addQuitHandler()
this.emit(UPDATE_DOWNLOADED, this.versionInfo)
Expand Down Expand Up @@ -123,9 +117,7 @@ export class NsisUpdater extends AppUpdater {
delete data.Path

const result = JSON.stringify(data, (name, value) => name === "RawData" ? undefined : value, 2)
if (this.logger != null) {
this.logger.info(`Sign verification failed, installer signed with incorrect certificate: ${result}`)
}
this._logger.info(`Sign verification failed, installer signed with incorrect certificate: ${result}`)
resolve(result)
})
})
Expand All @@ -140,9 +132,7 @@ export class NsisUpdater extends AppUpdater {
this.quitHandlerAdded = true

this.app.on("quit", () => {
if (this.logger != null) {
this.logger.info("Auto install update on quit")
}
this._logger.info("Auto install update on quit")
this.install(true)
})
}
Expand Down Expand Up @@ -185,10 +175,7 @@ export class NsisUpdater extends AppUpdater {
// yes, such errors dispatched not as error event
// https://github.com/electron-userland/electron-builder/issues/1129
if ((<any>e).code === "UNKNOWN" || (<any>e).code === "EACCES") { // Node 8 sends errors: https://nodejs.org/dist/latest-v8.x/docs/api/errors.html#errors_common_system_errors
if (this.logger != null) {
this.logger.info("Access denied or UNKNOWN error code on spawn, will be executed again using elevate")
}

this._logger.info("Access denied or UNKNOWN error code on spawn, will be executed again using elevate")
try {
spawn(path.join(process.resourcesPath!, "elevate.exe"), [setupPath].concat(args), spawnOptions)
.unref()
Expand Down
2 changes: 1 addition & 1 deletion packages/electron-updater/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface FileInfo {
export abstract class Provider<T extends VersionInfo> {
protected requestHeaders: RequestHeaders | null

setRequestHeaders(value: RequestHeaders | null) {
setRequestHeaders(value: RequestHeaders | null): void {
this.requestHeaders = value
}

Expand Down
3 changes: 2 additions & 1 deletion packages/electron-updater/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"files": [
"../../typings/semver.d.ts",
"../../typings/debug.d.ts",
"../../typings/electron.d.ts"
"../../typings/electron.d.ts",
"../../typings/uuid-1345.d.ts"
],
"include": [
"src/**/*.ts"
Expand Down
Loading

0 comments on commit d3526ee

Please sign in to comment.