diff --git a/.gitignore b/.gitignore index 6eec21c2a..bce6c56d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ debug.log docs/output docs/includes spec/fixtures/evil-files/ +out/ diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 1f8eb08e7..6685d4060 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -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' diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js new file mode 100644 index 000000000..6f7dbbb1a --- /dev/null +++ b/spec/auto-update-manager-spec.js @@ -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) + }) + }) + }) +}) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index cc1f4c946..3aff9e457 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -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') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 0ee12fe93..06296fbaf 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -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}") diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js new file mode 100644 index 000000000..62cc03f85 --- /dev/null +++ b/src/auto-update-manager.js @@ -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 + } +} diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 40b04c3a1..fdfea5474 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -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 [ diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee index 2df338761..c8c57cb01 100644 --- a/src/browser/auto-update-manager.coffee +++ b/src/browser/auto-update-manager.coffee @@ -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) ->