Merge pull request #10675 from atom/dh-expose-updates

Expose application updater lifecycle events to packages
This commit is contained in:
Ben Ogle
2016-03-01 15:31:56 -08:00
8 changed files with 290 additions and 10 deletions

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ debug.log
docs/output
docs/includes
spec/fixtures/evil-files/
out/

View File

@@ -328,3 +328,18 @@ describe "AtomEnvironment", ->
runs ->
{releaseVersion} = updateAvailableHandler.mostRecentCall.args[0]
expect(releaseVersion).toBe 'version'
describe "::getReleaseChannel()", ->
[version] = []
beforeEach ->
spyOn(atom, 'getVersion').andCallFake -> version
it "returns the correct channel based on the version number", ->
version = '1.5.6'
expect(atom.getReleaseChannel()).toBe 'stable'
version = '1.5.0-beta10'
expect(atom.getReleaseChannel()).toBe 'beta'
version = '1.7.0-dev-5340c91'
expect(atom.getReleaseChannel()).toBe 'dev'

View File

@@ -0,0 +1,115 @@
'use babel'
import AutoUpdateManager from '../src/auto-update-manager'
import {remote} from 'electron'
const electronAutoUpdater = remote.require('electron').autoUpdater
describe('AutoUpdateManager (renderer)', () => {
let autoUpdateManager
beforeEach(() => {
autoUpdateManager = new AutoUpdateManager({
applicationDelegate: atom.applicationDelegate
})
})
afterEach(() => {
autoUpdateManager.destroy()
})
describe('::onDidBeginCheckingForUpdate', () => {
it('subscribes to "did-begin-checking-for-update" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onDidBeginCheckingForUpdate(spy)
electronAutoUpdater.emit('checking-for-update')
waitsFor(() => {
return spy.callCount === 1
})
})
})
describe('::onDidBeginDownloadingUpdate', () => {
it('subscribes to "did-begin-downloading-update" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onDidBeginDownloadingUpdate(spy)
electronAutoUpdater.emit('update-available')
waitsFor(() => {
return spy.callCount === 1
})
})
})
describe('::onDidCompleteDownloadingUpdate', () => {
it('subscribes to "did-complete-downloading-update" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onDidCompleteDownloadingUpdate(spy)
electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3')
waitsFor(() => {
return spy.callCount === 1
})
runs(() => {
expect(spy.mostRecentCall.args[0].releaseVersion).toBe('1.2.3')
})
})
})
describe('::onUpdateNotAvailable', () => {
it('subscribes to "update-not-available" event', () => {
const spy = jasmine.createSpy('spy')
autoUpdateManager.onUpdateNotAvailable(spy)
electronAutoUpdater.emit('update-not-available')
waitsFor(() => {
return spy.callCount === 1
})
})
})
describe('::platformSupportsUpdates', () => {
let state, releaseChannel
it('returns true on OS X and Windows when in stable', () => {
spyOn(autoUpdateManager, 'getState').andCallFake(() => state)
spyOn(atom, 'getReleaseChannel').andCallFake(() => releaseChannel)
state = 'idle'
releaseChannel = 'stable'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(true)
state = 'idle'
releaseChannel = 'dev'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
state = 'unsupported'
releaseChannel = 'stable'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
state = 'unsupported'
releaseChannel = 'dev'
expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
})
})
describe('::destroy', () => {
it('unsubscribes from all events', () => {
const spy = jasmine.createSpy('spy')
const doneIndicator = jasmine.createSpy('spy')
atom.applicationDelegate.onUpdateNotAvailable(doneIndicator)
autoUpdateManager.onDidBeginCheckingForUpdate(spy)
autoUpdateManager.onDidBeginDownloadingUpdate(spy)
autoUpdateManager.onDidCompleteDownloadingUpdate(spy)
autoUpdateManager.onUpdateNotAvailable(spy)
autoUpdateManager.destroy()
electronAutoUpdater.emit('checking-for-update')
electronAutoUpdater.emit('update-available')
electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3')
electronAutoUpdater.emit('update-not-available')
waitsFor(() => {
return doneIndicator.callCount === 1
})
runs(() => {
expect(spy.callCount).toBe(0)
})
})
})
})

View File

@@ -166,8 +166,7 @@ class ApplicationDelegate
onDidOpenLocations: (callback) ->
outerCallback = (event, message, detail) ->
if message is 'open-locations'
callback(detail)
callback(detail) if message is 'open-locations'
ipcRenderer.on('message', outerCallback)
new Disposable ->
@@ -175,8 +174,38 @@ class ApplicationDelegate
onUpdateAvailable: (callback) ->
outerCallback = (event, message, detail) ->
if message is 'update-available'
callback(detail)
# TODO: Yes, this is strange that `onUpdateAvailable` is listening for
# `did-begin-downloading-update`. We currently have no mechanism to know
# if there is an update, so begin of downloading is a good proxy.
callback(detail) if message is 'did-begin-downloading-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidBeginDownloadingUpdate: (callback) ->
@onUpdateAvailable(callback)
onDidBeginCheckingForUpdate: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'checking-for-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidCompleteDownloadingUpdate: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: We could rename this event to `did-complete-downloading-update`
callback(detail) if message is 'update-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateNotAvailable: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-not-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
@@ -206,3 +235,12 @@ class ApplicationDelegate
disablePinchToZoom: ->
webFrame.setZoomLevelLimits(1, 1)
checkForUpdate: ->
ipcRenderer.send('check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')

View File

@@ -41,6 +41,7 @@ TextEditor = require './text-editor'
TextBuffer = require 'text-buffer'
Gutter = require './gutter'
TextEditorRegistry = require './text-editor-registry'
AutoUpdateManager = require './auto-update-manager'
WorkspaceElement = require './workspace-element'
PanelContainerElement = require './panel-container-element'
@@ -115,6 +116,9 @@ class AtomEnvironment extends Model
# Public: A {TextEditorRegistry} instance
textEditors: null
# Private: An {AutoUpdateManager} instance
autoUpdater: null
saveStateDebounceInterval: 1000
###
@@ -188,6 +192,7 @@ class AtomEnvironment extends Model
@themes.workspace = @workspace
@textEditors = new TextEditorRegistry
@autoUpdater = new AutoUpdateManager({@applicationDelegate})
@config.load()
@@ -333,6 +338,7 @@ class AtomEnvironment extends Model
@commands.clear()
@stylesElement.remove()
@config.unobserveUserConfig()
@autoUpdater.destroy()
@uninstallWindowEventHandler()
@@ -411,6 +417,16 @@ class AtomEnvironment extends Model
getVersion: ->
@appVersion ?= @getLoadSettings().appVersion
# Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'`
getReleaseChannel: ->
version = @getVersion()
if version.indexOf('beta') > -1
'beta'
else if version.indexOf('dev') > -1
'dev'
else
'stable'
# Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@@ -830,7 +846,6 @@ class AtomEnvironment extends Model
@applicationDelegate.setTemporaryWindowState(state)
savePromise.catch(reject).then(resolve)
loadState: ->
if @enablePersistence
if stateKey = @getStateKey(@getLoadSettings().initialPaths)
@@ -879,6 +894,7 @@ class AtomEnvironment extends Model
detail: error.message
dismissable: true
# TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
@@ -886,7 +902,8 @@ class AtomEnvironment extends Model
@emitter.emit 'update-available', details
listenForUpdates: ->
@disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this)))
# listen for updates available locally (that have been successfully downloaded)
@disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this)))
setBodyPlatformClass: ->
@document.body.classList.add("platform-#{process.platform}")

View File

@@ -0,0 +1,73 @@
'use babel'
import {Emitter, CompositeDisposable} from 'event-kit'
export default class AutoUpdateManager {
constructor ({applicationDelegate}) {
this.applicationDelegate = applicationDelegate
this.subscriptions = new CompositeDisposable()
this.emitter = new Emitter()
this.subscriptions.add(
applicationDelegate.onDidBeginCheckingForUpdate(() => {
this.emitter.emit('did-begin-checking-for-update')
}),
applicationDelegate.onDidBeginDownloadingUpdate(() => {
this.emitter.emit('did-begin-downloading-update')
}),
applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
this.emitter.emit('did-complete-downloading-update', details)
}),
applicationDelegate.onUpdateNotAvailable(() => {
this.emitter.emit('update-not-available')
})
)
}
destroy () {
this.subscriptions.dispose()
this.emitter.dispose()
}
checkForUpdate () {
this.applicationDelegate.checkForUpdate()
}
restartAndInstallUpdate () {
this.applicationDelegate.restartAndInstallUpdate()
}
getState () {
return this.applicationDelegate.getAutoUpdateManagerState()
}
platformSupportsUpdates () {
return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
}
onDidBeginCheckingForUpdate (callback) {
return this.emitter.on('did-begin-checking-for-update', callback)
}
onDidBeginDownloadingUpdate (callback) {
return this.emitter.on('did-begin-downloading-update', callback)
}
onDidCompleteDownloadingUpdate (callback) {
return this.emitter.on('did-complete-downloading-update', callback)
}
// TODO: When https://github.com/atom/electron/issues/4587 is closed, we can
// add an update-available event.
// onUpdateAvailable (callback) {
// return this.emitter.on('update-available', callback)
// }
onUpdateNotAvailable (callback) {
return this.emitter.on('update-not-available', callback)
}
getPlatform () {
return process.platform
}
}

View File

@@ -304,6 +304,15 @@ class AtomApplication
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
ipcMain.on 'check-for-update', =>
@autoUpdateManager.check()
ipcMain.on 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [

View File

@@ -39,16 +39,24 @@ class AutoUpdateManager
autoUpdater.on 'checking-for-update', =>
@setState(CheckingState)
@emitWindowEvent('checking-for-update')
autoUpdater.on 'update-not-available', =>
@setState(NoUpdateAvailableState)
@emitWindowEvent('update-not-available')
autoUpdater.on 'update-available', =>
@setState(DownladingState)
# We use sendMessage to send an event called 'update-available' in 'update-downloaded'
# once the update download is complete. This mismatch between the electron
# autoUpdater events is unfortunate but in the interest of not changing the
# one existing event handled by applicationDelegate
@emitWindowEvent('did-begin-downloading-update')
@emit('did-begin-download')
autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
@emitUpdateAvailableEvent(@getWindows()...)
@emitUpdateAvailableEvent()
@config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
if newValue
@@ -64,10 +72,14 @@ class AutoUpdateManager
when 'linux'
@setState(UnsupportedState)
emitUpdateAvailableEvent: (windows...) ->
emitUpdateAvailableEvent: ->
return unless @releaseVersion?
for atomWindow in windows
atomWindow.sendMessage('update-available', {@releaseVersion})
@emitWindowEvent('update-available', {@releaseVersion})
return
emitWindowEvent: (eventName, payload) ->
for atomWindow in @getWindows()
atomWindow.sendMessage(eventName, payload)
return
setState: (state) ->