diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 8630284bc1f63..15459f7dae80d 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -13,6 +13,7 @@ const _7z = require('7zip')['7z']; const util = require('./lib/util'); const pkg = require('../package.json'); const product = require('../product.json'); +const vfs = require('vinyl-fs'); const repoPath = path.dirname(__dirname); const buildPath = arch => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); @@ -77,7 +78,7 @@ gulp.task('vscode-win32-x64-setup', ['clean-vscode-win32-x64-setup'], buildWin32 function archiveWin32Setup(arch) { return cb => { - const args = ['a', '-tzip', zipPath(arch), '.', '-r']; + const args = ['a', '-tzip', zipPath(arch), '.', '-r', '-x!inno_updater.exe']; cp.spawn(_7z, args, { stdio: 'inherit', cwd: buildPath(arch) }) .on('error', cb) @@ -90,3 +91,13 @@ gulp.task('vscode-win32-ia32-archive', ['clean-vscode-win32-ia32-archive'], arch gulp.task('clean-vscode-win32-x64-archive', util.rimraf(zipDir('x64'))); gulp.task('vscode-win32-x64-archive', ['clean-vscode-win32-x64-archive'], archiveWin32Setup('x64')); + +function copyInnoUpdater(arch) { + return () => { + return gulp.src('build/win32/inno_updater.exe', { base: 'build/win32' }) + .pipe(vfs.dest(buildPath(arch))); + }; +} + +gulp.task('vscode-win32-ia32-copy-inno-updater', copyInnoUpdater('ia32')); +gulp.task('vscode-win32-x64-copy-inno-updater', copyInnoUpdater('x64')); \ No newline at end of file diff --git a/build/tfs/common/publish.ts b/build/tfs/common/publish.ts index e4cbdc80f0e7b..ca580ce1b4d05 100644 --- a/build/tfs/common/publish.ts +++ b/build/tfs/common/publish.ts @@ -69,6 +69,7 @@ interface Asset { hash: string; sha256hash: string; size: number; + supportsFastUpdate?: boolean; } function createOrUpdate(commit: string, quality: string, platform: string, type: string, release: NewDocument, asset: Asset, isUpdate: boolean): Promise { @@ -234,6 +235,13 @@ async function publish(commit: string, quality: string, platform: string, type: size }; + // Remove this if we ever need to rollback fast updates for windows + if (/win32/.test(platform)) { + asset.supportsFastUpdate = true; + } + + console.log('Asset:', JSON.stringify(asset, null, ' ')); + const release = { id: commit, timestamp: (new Date()).getTime(), diff --git a/build/tfs/win32/1_build.ps1 b/build/tfs/win32/1_build.ps1 index bc6ade13de33b..0fcb56c1b9e4a 100644 --- a/build/tfs/win32/1_build.ps1 +++ b/build/tfs/win32/1_build.ps1 @@ -45,6 +45,10 @@ step "Build minified" { exec { & npm run gulp -- "vscode-win32-$global:arch-min" } } +step "Copy Inno updater" { + exec { & npm run gulp -- "vscode-win32-$global:arch-copy-inno-updater" } +} + # step "Create loader snapshot" { # exec { & node build\lib\snapshotLoader.js --arch=$global:arch } # } diff --git a/build/win32/code.iss b/build/win32/code.iss index f4374ee099d51..1601f540fa2c3 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -18,7 +18,7 @@ OutputDir={#OutputDir} OutputBaseFilename=VSCodeSetup Compression=lzma SolidCompression=yes -AppMutex={#AppMutex} +AppMutex={code:GetAppMutex} SetupMutex={#AppMutex}setup WizardImageFile={#RepoDir}\resources\win32\inno-big.bmp WizardSmallImageFile={#RepoDir}\resources\win32\inno-small.bmp @@ -47,11 +47,15 @@ Name: "simplifiedChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh Name: "traditionalChinese"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.zh-tw.isl,{#RepoDir}\build\win32\i18n\messages.zh-tw.isl" {#LocalizedLanguageFile("cht")} [InstallDelete] -Type: filesandordirs; Name: {app}\resources\app\out -Type: filesandordirs; Name: {app}\resources\app\plugins -Type: filesandordirs; Name: {app}\resources\app\extensions -Type: filesandordirs; Name: {app}\resources\app\node_modules -Type: files; Name: {app}\resources\app\Credits_45.0.2454.85.html +Type: filesandordirs; Name: "{app}\resources\app\out"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\plugins"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\extensions"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\node_modules"; Check: IsNotUpdate +Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotUpdate + +[UninstallDelete] +Type: filesandordirs; Name: "{app}\_" +Type: filesandordirs; Name: "{app}\old" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked @@ -63,7 +67,8 @@ Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent [Files] -Source: "*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "*"; Excludes: "inno_updater.exe"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "inno_updater.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" @@ -71,7 +76,7 @@ Name: "{commondesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}" [Run] -Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: WizardSilent +Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Tasks: runcode; Flags: nowait postinstall; Check: ShouldRunAfterUpdate Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong}}"; Flags: nowait postinstall; Check: WizardNotSilent [Registry] @@ -955,6 +960,62 @@ begin Result := not WizardSilent(); end; +// Updates +function IsBackgroundUpdate(): Boolean; +begin + Result := ExpandConstant('{param:update|false}') <> 'false'; +end; + +function IsNotUpdate(): Boolean; +begin + Result := not IsBackgroundUpdate(); +end; + +function ShouldRunAfterUpdate(): Boolean; +begin + if IsBackgroundUpdate() then + // VS Code will create a flag file before the update starts (/update=C:\foo\bar) + // - if the file exists at this point, the user quit Code before the update finished, so don't start Code after update + // - otherwise, the user has accepted to apply the update and Code should start + Result := not FileExists(ExpandConstant('{param:update}')) + else + Result := True; +end; + +function GetAppMutex(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := '' + else + Result := '{#AppMutex}'; +end; + +function GetDestDir(Value: string): string; +begin + if IsBackgroundUpdate() then + Result := ExpandConstant('{app}\_') + else + Result := ExpandConstant('{app}'); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + UpdateResultCode: Integer; +begin + if IsBackgroundUpdate() and (CurStep = ssPostInstall) then + begin + CreateMutex('{#AppMutex}-ready'); + + while (CheckForMutexes('{#AppMutex}')) do + begin + Log('Application is still running, waiting'); + Sleep(1000); + end; + + Exec(ExpandConstant('{app}\inno_updater.exe'), ExpandConstant('--apply-update _ "{app}\unins000.dat"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + end; +end; + // http://stackoverflow.com/a/23838239/261019 procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); var diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe new file mode 100644 index 0000000000000..d82b1430c6407 Binary files /dev/null and b/build/win32/inno_updater.exe differ diff --git a/src/typings/windows-mutex.ts b/src/typings/windows-mutex.ts index 039dffcc2ad56..a3acc8f4430ce 100644 --- a/src/typings/windows-mutex.ts +++ b/src/typings/windows-mutex.ts @@ -9,4 +9,6 @@ declare module 'windows-mutex' { isActive(): boolean; release(): void; } + + export function isActive(name: string): boolean; } \ No newline at end of file diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 46fbe9ce0b2fd..24fb6d6ca51d8 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -16,7 +16,6 @@ import { CodeMenu } from 'vs/code/electron-main/menus'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/common/updateIpc'; -import { UpdateService } from 'vs/platform/update/electron-main/updateService'; import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main'; import { Server, connect, Client } from 'vs/base/parts/ipc/node/ipc.net'; import { SharedProcess } from 'vs/code/electron-main/sharedProcess'; @@ -52,6 +51,9 @@ import URI from 'vs/base/common/uri'; import { WorkspacesChannel } from 'vs/platform/workspaces/common/workspacesIpc'; import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import { getMachineId } from 'vs/base/node/id'; +import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; +import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; +import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; import { IIssueService } from 'vs/platform/issue/common/issue'; import { IssueChannel } from 'vs/platform/issue/common/issueIpc'; import { IssueService } from 'vs/platform/issue/electron-main/issueService'; @@ -307,7 +309,14 @@ export class CodeApplication { private initServices(machineId: string): IInstantiationService { const services = new ServiceCollection(); - services.set(IUpdateService, new SyncDescriptor(UpdateService)); + if (process.platform === 'win32') { + services.set(IUpdateService, new SyncDescriptor(Win32UpdateService)); + } else if (process.platform === 'linux') { + services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService)); + } else if (process.platform === 'darwin') { + services.set(IUpdateService, new SyncDescriptor(DarwinUpdateService)); + } + services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, machineId)); services.set(IWindowsService, new SyncDescriptor(WindowsService, this.sharedProcess)); services.set(ILaunchService, new SyncDescriptor(LaunchService)); diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index aafae0c8ed99c..7e389c637293a 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -14,7 +14,7 @@ import { OpenContext, IRunActionInWindowRequest } from 'vs/platform/windows/comm import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IUpdateService, State as UpdateState } from 'vs/platform/update/common/update'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import product from 'vs/platform/node/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -1040,45 +1040,51 @@ export class CodeMenu { } private getUpdateMenuItems(): Electron.MenuItem[] { - switch (this.updateService.state) { - case UpdateState.Uninitialized: + const state = this.updateService.state; + + switch (state.type) { + case StateType.Uninitialized: return []; - case UpdateState.UpdateDownloaded: + case StateType.Idle: return [new MenuItem({ - label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { - this.reportMenuActionTelemetry('RestartToUpdate'); - this.updateService.quitAndInstall(); - } + label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { + this.reportMenuActionTelemetry('CheckForUpdate'); + this.updateService.checkForUpdates(true); + }, 0) })]; - case UpdateState.CheckingForUpdate: + case StateType.CheckingForUpdates: return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })]; - case UpdateState.UpdateAvailable: - if (isLinux) { - return [new MenuItem({ - label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { - this.updateService.quitAndInstall(); - } - })]; - } - - const updateAvailableLabel = isWindows - ? nls.localize('miDownloadingUpdate', "Downloading Update...") - : nls.localize('miInstallingUpdate', "Installing Update..."); + case StateType.AvailableForDownload: + return [new MenuItem({ + label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { + this.updateService.downloadUpdate(); + } + })]; - return [new MenuItem({ label: updateAvailableLabel, enabled: false })]; + case StateType.Downloading: + return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; - default: - const result = [new MenuItem({ - label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { - this.reportMenuActionTelemetry('CheckForUpdate'); - this.updateService.checkForUpdates(true); - }, 0) + case StateType.Downloaded: + return [new MenuItem({ + label: nls.localize('miInstallUpdate', "Install Update..."), click: () => { + this.reportMenuActionTelemetry('InstallUpdate'); + this.updateService.applyUpdate(); + } })]; - return result; + case StateType.Updating: + return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })]; + + case StateType.Ready: + return [new MenuItem({ + label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { + this.reportMenuActionTelemetry('RestartToUpdate'); + this.updateService.quitAndInstall(); + } + })]; } } diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 13928e33f1784..8ce018cb411f7 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -9,35 +9,71 @@ import Event, { NodeEventEmitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -export enum State { - Uninitialized, - Idle, - CheckingForUpdate, - UpdateAvailable, - UpdateDownloaded -} - -export enum ExplicitState { - Implicit, - Explicit -} - -export interface IRawUpdate { - releaseNotes: string; - version: string; - date: Date; -} - export interface IUpdate { version: string; + productVersion: string; date?: Date; releaseNotes?: string; + supportsFastUpdate?: boolean; url?: string; + hash?: string; } +/** + * Updates are run as a state machine: + * + * Uninitialized + * ↓ + * Idle + * ↓ ↑ + * Checking for Updates → Available for Download + * ↓ + * Downloading → Ready + * ↓ ↑ + * Downloaded → Updating + * + * Available: There is an update available for download (linux). + * Ready: Code will be updated as soon as it restarts (win32, darwin). + * Donwloaded: There is an update ready to be installed in the background (win32). + */ + +export enum StateType { + Uninitialized = 'uninitialized', + Idle = 'idle', + CheckingForUpdates = 'checking for updates', + AvailableForDownload = 'available for download', + Downloading = 'downloading', + Downloaded = 'downloaded', + Updating = 'updating', + Ready = 'ready', +} + +export type Uninitialized = { type: StateType.Uninitialized }; +export type Idle = { type: StateType.Idle }; +export type CheckingForUpdates = { type: StateType.CheckingForUpdates, explicit: boolean }; +export type AvailableForDownload = { type: StateType.AvailableForDownload, update: IUpdate }; +export type Downloading = { type: StateType.Downloading, update: IUpdate }; +export type Downloaded = { type: StateType.Downloaded, update: IUpdate }; +export type Updating = { type: StateType.Updating, update: IUpdate }; +export type Ready = { type: StateType.Ready, update: IUpdate }; + +export type State = Uninitialized | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready; + +export const State = { + Uninitialized: { type: StateType.Uninitialized } as Uninitialized, + Idle: { type: StateType.Idle } as Idle, + CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), + AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), + Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), + Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), + Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), + Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), +}; + export interface IAutoUpdater extends NodeEventEmitter { setFeedURL(url: string): void; checkForUpdates(): void; + applyUpdate?(): TPromise; quitAndInstall(): void; } @@ -46,13 +82,11 @@ export const IUpdateService = createDecorator('updateService'); export interface IUpdateService { _serviceBrand: any; - readonly onError: Event; - readonly onUpdateAvailable: Event<{ url: string; version: string; }>; - readonly onUpdateNotAvailable: Event; - readonly onUpdateReady: Event; readonly onStateChange: Event; readonly state: State; - checkForUpdates(explicit: boolean): TPromise; + checkForUpdates(explicit: boolean): TPromise; + downloadUpdate(): TPromise; + applyUpdate(): TPromise; quitAndInstall(): TPromise; } \ No newline at end of file diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index bd42c38e018cf..441bdd138e139 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -9,15 +9,12 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel, eventToCall, eventFromCall } from 'vs/base/parts/ipc/common/ipc'; import Event, { Emitter } from 'vs/base/common/event'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { IUpdateService, IRawUpdate, State, IUpdate } from './update'; +import { IUpdateService, State } from './update'; export interface IUpdateChannel extends IChannel { - call(command: 'event:onError'): TPromise; - call(command: 'event:onUpdateAvailable'): TPromise; - call(command: 'event:onUpdateNotAvailable'): TPromise; - call(command: 'event:onUpdateReady'): TPromise; - call(command: 'event:onStateChange'): TPromise; - call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'checkForUpdates', arg: boolean): TPromise; + call(command: 'downloadUpdate'): TPromise; + call(command: 'applyUpdate'): TPromise; call(command: 'quitAndInstall'): TPromise; call(command: '_getInitialState'): TPromise; call(command: string, arg?: any): TPromise; @@ -29,12 +26,10 @@ export class UpdateChannel implements IUpdateChannel { call(command: string, arg?: any): TPromise { switch (command) { - case 'event:onError': return eventToCall(this.service.onError); - case 'event:onUpdateAvailable': return eventToCall(this.service.onUpdateAvailable); - case 'event:onUpdateNotAvailable': return eventToCall(this.service.onUpdateNotAvailable); - case 'event:onUpdateReady': return eventToCall(this.service.onUpdateReady); case 'event:onStateChange': return eventToCall(this.service.onStateChange); case 'checkForUpdates': return this.service.checkForUpdates(arg); + case 'downloadUpdate': return this.service.downloadUpdate(); + case 'applyUpdate': return this.service.applyUpdate(); case 'quitAndInstall': return this.service.quitAndInstall(); case '_getInitialState': return TPromise.as(this.service.state); } @@ -46,19 +41,8 @@ export class UpdateChannelClient implements IUpdateService { _serviceBrand: any; - private _onError = eventFromCall(this.channel, 'event:onError'); - get onError(): Event { return this._onError; } - - private _onUpdateAvailable = eventFromCall<{ url: string; version: string; }>(this.channel, 'event:onUpdateAvailable'); - get onUpdateAvailable(): Event<{ url: string; version: string; }> { return this._onUpdateAvailable; } - - private _onUpdateNotAvailable = eventFromCall(this.channel, 'event:onUpdateNotAvailable'); - get onUpdateNotAvailable(): Event { return this._onUpdateNotAvailable; } - - private _onUpdateReady = eventFromCall(this.channel, 'event:onUpdateReady'); - get onUpdateReady(): Event { return this._onUpdateReady; } - private _onRemoteStateChange = eventFromCall(this.channel, 'event:onStateChange'); + private _onStateChange = new Emitter(); get onStateChange(): Event { return this._onStateChange.event; } @@ -78,10 +62,18 @@ export class UpdateChannelClient implements IUpdateService { }, onUnexpectedError); } - checkForUpdates(explicit: boolean): TPromise { + checkForUpdates(explicit: boolean): TPromise { return this.channel.call('checkForUpdates', explicit); } + downloadUpdate(): TPromise { + return this.channel.call('downloadUpdate'); + } + + applyUpdate(): TPromise { + return this.channel.call('applyUpdate'); + } + quitAndInstall(): TPromise { return this.channel.call('quitAndInstall'); } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts new file mode 100644 index 0000000000000..fcc47b9f7a684 --- /dev/null +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import Event, { Emitter } from 'vs/base/common/event'; +import { Throttler } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import product from 'vs/platform/node/product'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IUpdateService, State, StateType, AvailableForDownload } from 'vs/platform/update/common/update'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; + +export function createUpdateURL(platform: string, quality: string): string { + return `${product.updateUrl}/api/update/${platform}/${quality}/${product.commit}`; +} + +export abstract class AbstractUpdateService implements IUpdateService { + + _serviceBrand: any; + + private _state: State = State.Uninitialized; + private throttler: Throttler = new Throttler(); + + private _onStateChange = new Emitter(); + get onStateChange(): Event { return this._onStateChange.event; } + + get state(): State { + return this._state; + } + + protected setState(state: State): void { + this.logService.info('update#setState', state.type); + this._state = state; + this._onStateChange.fire(state); + } + + constructor( + @ILifecycleService private lifecycleService: ILifecycleService, + @IConfigurationService protected configurationService: IConfigurationService, + @IEnvironmentService private environmentService: IEnvironmentService, + @ILogService protected logService: ILogService + ) { + if (this.environmentService.disableUpdates) { + this.logService.info('update#ctor - updates are disabled'); + return; + } + + if (!product.updateUrl || !product.commit) { + this.logService.info('update#ctor - updates are disabled'); + return; + } + + const quality = this.getProductQuality(); + + if (!quality) { + this.logService.info('update#ctor - updates are disabled'); + return; + } + + if (!this.setUpdateFeedUrl(quality)) { + this.logService.info('update#ctor - updates are disabled'); + return; + } + + this.setState({ type: StateType.Idle }); + + // Start checking for updates after 30 seconds + this.scheduleCheckForUpdates(30 * 1000) + .done(null, err => this.logService.error(err)); + } + + private getProductQuality(): string { + const quality = this.configurationService.getValue('update.channel'); + return quality === 'none' ? null : product.quality; + } + + private scheduleCheckForUpdates(delay = 60 * 60 * 1000): TPromise { + return TPromise.timeout(delay) + .then(() => this.checkForUpdates()) + .then(update => { + if (update) { + // Update found, no need to check more + return TPromise.as(null); + } + + // Check again after 1 hour + return this.scheduleCheckForUpdates(60 * 60 * 1000); + }); + } + + checkForUpdates(explicit = false): TPromise { + this.logService.trace('update#checkForUpdates, state = ', this.state.type); + + if (this.state.type !== StateType.Idle) { + return TPromise.as(null); + } + + return this.throttler.queue(() => TPromise.as(this.doCheckForUpdates(explicit))); + } + + downloadUpdate(): TPromise { + this.logService.trace('update#downloadUpdate, state = ', this.state.type); + + if (this.state.type !== StateType.AvailableForDownload) { + return TPromise.as(null); + } + + return this.doDownloadUpdate(this.state); + } + + protected doDownloadUpdate(state: AvailableForDownload): TPromise { + return TPromise.as(null); + } + + applyUpdate(): TPromise { + this.logService.trace('update#applyUpdate, state = ', this.state.type); + + if (this.state.type !== StateType.Ready) { + return TPromise.as(null); + } + + return this.doApplyUpdate(); + } + + protected doApplyUpdate(): TPromise { + return TPromise.as(null); + } + + quitAndInstall(): TPromise { + this.logService.trace('update#quitAndInstall, state = ', this.state.type); + + if (this.state.type !== StateType.Ready) { + return TPromise.as(null); + } + + this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); + + this.lifecycleService.quit(true /* from update */).done(vetod => { + this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); + if (vetod) { + return; + } + + this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); + this.doQuitAndInstall(); + }); + + return TPromise.as(null); + } + + protected doQuitAndInstall(): void { + // noop + } + + protected abstract setUpdateFeedUrl(quality: string): boolean; + protected abstract doCheckForUpdates(explicit: boolean): void; +} diff --git a/src/vs/platform/update/electron-main/auto-updater.linux.ts b/src/vs/platform/update/electron-main/auto-updater.linux.ts deleted file mode 100644 index bdc9c183329c4..0000000000000 --- a/src/vs/platform/update/electron-main/auto-updater.linux.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import { EventEmitter } from 'events'; -import { isString } from 'vs/base/common/types'; -import { Promise } from 'vs/base/common/winjs.base'; -import { asJson } from 'vs/base/node/request'; -import { IRequestService } from 'vs/platform/request/node/request'; -import { IAutoUpdater } from 'vs/platform/update/common/update'; -import product from 'vs/platform/node/product'; - -interface IUpdate { - url: string; - name: string; - releaseNotes?: string; - version: string; - productVersion: string; - hash: string; -} - -export class LinuxAutoUpdaterImpl extends EventEmitter implements IAutoUpdater { - - private url: string; - private currentRequest: Promise; - - constructor( - @IRequestService private requestService: IRequestService - ) { - super(); - - this.url = null; - this.currentRequest = null; - } - - setFeedURL(url: string): void { - this.url = url; - } - - checkForUpdates(): void { - if (!this.url) { - throw new Error('No feed url set.'); - } - - if (this.currentRequest) { - return; - } - - this.emit('checking-for-update'); - - this.currentRequest = this.requestService.request({ url: this.url }) - .then(asJson) - .then(update => { - if (!update || !update.url || !update.version || !update.productVersion) { - this.emit('update-not-available'); - } else { - this.emit('update-available', null, product.downloadUrl, update.productVersion); - } - }) - .then(null, e => { - if (isString(e) && /^Server returned/.test(e)) { - return; - } - - this.emit('update-not-available'); - this.emit('error', e); - }) - .then(() => this.currentRequest = null); - } - - quitAndInstall(): void { - // noop - } -} diff --git a/src/vs/platform/update/electron-main/auto-updater.win32.ts b/src/vs/platform/update/electron-main/auto-updater.win32.ts deleted file mode 100644 index c88880617f32f..0000000000000 --- a/src/vs/platform/update/electron-main/auto-updater.win32.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as path from 'path'; -import * as pfs from 'vs/base/node/pfs'; -import { checksum } from 'vs/base/node/crypto'; -import { EventEmitter } from 'events'; -import { tmpdir } from 'os'; -import { spawn } from 'child_process'; -import { isString } from 'vs/base/common/types'; -import { Promise, TPromise } from 'vs/base/common/winjs.base'; -import { download, asJson } from 'vs/base/node/request'; -import { IRequestService } from 'vs/platform/request/node/request'; -import { IAutoUpdater } from 'vs/platform/update/common/update'; -import product from 'vs/platform/node/product'; - -interface IUpdate { - url: string; - name: string; - releaseNotes?: string; - version: string; - productVersion: string; - hash: string; -} - -export class Win32AutoUpdaterImpl extends EventEmitter implements IAutoUpdater { - - private url: string = null; - private currentRequest: Promise = null; - private updatePackagePath: string = null; - - constructor( - @IRequestService private requestService: IRequestService - ) { - super(); - } - - get cachePath(): TPromise { - const result = path.join(tmpdir(), `vscode-update-${process.arch}`); - return pfs.mkdirp(result, null).then(() => result); - } - - setFeedURL(url: string): void { - this.url = url; - } - - checkForUpdates(): void { - if (!this.url) { - throw new Error('No feed url set.'); - } - - if (this.currentRequest) { - return; - } - - this.emit('checking-for-update'); - - this.currentRequest = this.requestService.request({ url: this.url }) - .then(asJson) - .then(update => { - if (!update || !update.url || !update.version) { - this.emit('update-not-available'); - return this.cleanup(); - } - - this.emit('update-available'); - - return this.cleanup(update.version).then(() => { - return this.getUpdatePackagePath(update.version).then(updatePackagePath => { - return pfs.exists(updatePackagePath).then(exists => { - if (exists) { - return TPromise.as(updatePackagePath); - } - - const url = update.url; - const hash = update.hash; - const downloadPath = `${updatePackagePath}.tmp`; - - return this.requestService.request({ url }) - .then(context => download(downloadPath, context)) - .then(hash ? () => checksum(downloadPath, update.hash) : () => null) - .then(() => pfs.rename(downloadPath, updatePackagePath)) - .then(() => updatePackagePath); - }); - }).then(updatePackagePath => { - this.updatePackagePath = updatePackagePath; - - this.emit('update-downloaded', - {}, - update.releaseNotes, - update.productVersion, - new Date(), - this.url - ); - }); - }); - }) - .then(null, e => { - if (isString(e) && /^Server returned/.test(e)) { - return; - } - - this.emit('update-not-available'); - this.emit('error', e); - }) - .then(() => this.currentRequest = null); - } - - private getUpdatePackagePath(version: string): TPromise { - return this.cachePath.then(cachePath => path.join(cachePath, `CodeSetup-${product.quality}-${version}.exe`)); - } - - private cleanup(exceptVersion: string = null): Promise { - const filter = exceptVersion ? one => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; - - return this.cachePath - .then(cachePath => pfs.readdir(cachePath) - .then(all => Promise.join(all - .filter(filter) - .map(one => pfs.unlink(path.join(cachePath, one)).then(null, () => null)) - )) - ); - } - - quitAndInstall(): void { - if (!this.updatePackagePath) { - return; - } - - spawn(this.updatePackagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { - detached: true, - stdio: ['ignore', 'ignore', 'ignore'] - }); - } -} diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts new file mode 100644 index 0000000000000..88b0f19310221 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as electron from 'electron'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import Event, { fromNodeEventEmitter } from 'vs/base/common/event'; +import { memoize } from 'vs/base/common/decorators'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { AbstractUpdateService, createUpdateURL } from 'vs/platform/update/electron-main/abstractUpdateService'; + +export class DarwinUpdateService extends AbstractUpdateService { + + _serviceBrand: any; + + private disposables: IDisposable[] = []; + + @memoize private get onRawError(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } + @memoize private get onRawUpdateNotAvailable(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } + @memoize private get onRawUpdateAvailable(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } + @memoize private get onRawUpdateDownloaded(): Event { return fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, + @ILogService logService: ILogService + ) { + super(lifecycleService, configurationService, environmentService, logService); + this.onRawError(this.logService.error, this.logService, this.disposables); + this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); + this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); + this.onRawUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); + } + + protected setUpdateFeedUrl(quality: string): boolean { + try { + electron.autoUpdater.setFeedURL(createUpdateURL('darwin', quality)); + } catch (e) { + // application is very likely not signed + this.logService.error('Failed to set update feed URL'); + return false; + } + + return true; + } + + protected doCheckForUpdates(explicit: boolean): void { + this.setState(State.CheckingForUpdates(explicit)); + electron.autoUpdater.checkForUpdates(); + } + + private onUpdateAvailable(update: IUpdate): void { + if (this.state.type !== StateType.CheckingForUpdates) { + return; + } + + this.setState(State.Downloading(update)); + } + + private onUpdateDownloaded(update: IUpdate): void { + if (this.state.type !== StateType.Downloading) { + return; + } + + /* __GDPR__ + "update:downloaded" : { + "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:downloaded', { version: update.version }); + + this.setState(State.Ready(update)); + } + + private onUpdateNotAvailable(): void { + if (this.state.type !== StateType.CheckingForUpdates) { + return; + } + + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit: this.state.explicit }); + + this.setState(State.Idle); + } + + protected doQuitAndInstall(): void { + // for some reason updating on Mac causes the local storage not to be flushed. + // we workaround this issue by forcing an explicit flush of the storage data. + // see also https://github.com/Microsoft/vscode/issues/172 + this.logService.trace('update#quitAndInstall(): calling flushStorageData()'); + electron.session.defaultSession.flushStorageData(); + + this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); + electron.autoUpdater.quitAndInstall(); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts new file mode 100644 index 0000000000000..cb36c65858561 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { IRequestService } from 'vs/platform/request/node/request'; +import { State, IUpdate, AvailableForDownload } from 'vs/platform/update/common/update'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createUpdateURL, AbstractUpdateService } from 'vs/platform/update/electron-main/abstractUpdateService'; +import { asJson } from 'vs/base/node/request'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { shell } from 'electron'; + +export class LinuxUpdateService extends AbstractUpdateService { + + _serviceBrand: any; + + private url: string | undefined; + + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, + @IRequestService private requestService: IRequestService, + @ILogService logService: ILogService + ) { + super(lifecycleService, configurationService, environmentService, logService); + } + + protected setUpdateFeedUrl(quality: string): boolean { + this.url = createUpdateURL(`linux-${process.arch}`, quality); + return true; + } + + protected doCheckForUpdates(explicit: boolean): void { + if (!this.url) { + return; + } + + this.setState(State.CheckingForUpdates(explicit)); + + this.requestService.request({ url: this.url }) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version || !update.productVersion) { + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + + this.setState(State.Idle); + } else { + this.setState(State.AvailableForDownload(update)); + } + }) + .then(null, err => { + this.logService.error(err); + + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + this.setState(State.Idle); + }); + } + + protected doDownloadUpdate(state: AvailableForDownload): TPromise { + shell.openExternal(state.update.url); + this.setState(State.Idle); + + return TPromise.as(null); + } +} diff --git a/src/vs/platform/update/electron-main/updateService.ts b/src/vs/platform/update/electron-main/updateService.ts deleted file mode 100644 index b7f8f5edf69a1..0000000000000 --- a/src/vs/platform/update/electron-main/updateService.ts +++ /dev/null @@ -1,295 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as fs from 'original-fs'; -import * as path from 'path'; -import * as electron from 'electron'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import Event, { Emitter, once, filterEvent, fromNodeEventEmitter } from 'vs/base/common/event'; -import { always, Throttler } from 'vs/base/common/async'; -import { memoize } from 'vs/base/common/decorators'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { Win32AutoUpdaterImpl } from './auto-updater.win32'; -import { LinuxAutoUpdaterImpl } from './auto-updater.linux'; -import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import { IRequestService } from 'vs/platform/request/node/request'; -import product from 'vs/platform/node/product'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IUpdateService, State, IAutoUpdater, IUpdate, IRawUpdate } from 'vs/platform/update/common/update'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILogService } from 'vs/platform/log/common/log'; - -export class UpdateService implements IUpdateService { - - _serviceBrand: any; - - private _state: State = State.Uninitialized; - private _availableUpdate: IUpdate = null; - private raw: IAutoUpdater; - private throttler: Throttler = new Throttler(); - - private _onError = new Emitter(); - get onError(): Event { return this._onError.event; } - - private _onCheckForUpdate = new Emitter(); - get onCheckForUpdate(): Event { return this._onCheckForUpdate.event; } - - private _onUpdateAvailable = new Emitter<{ url: string; version: string; }>(); - get onUpdateAvailable(): Event<{ url: string; version: string; }> { return this._onUpdateAvailable.event; } - - private _onUpdateNotAvailable = new Emitter(); - get onUpdateNotAvailable(): Event { return this._onUpdateNotAvailable.event; } - - private _onUpdateReady = new Emitter(); - get onUpdateReady(): Event { return this._onUpdateReady.event; } - - private _onStateChange = new Emitter(); - get onStateChange(): Event { return this._onStateChange.event; } - - @memoize - private get onRawError(): Event { - return fromNodeEventEmitter(this.raw, 'error', (_, message) => message); - } - - @memoize - private get onRawUpdateNotAvailable(): Event { - return fromNodeEventEmitter(this.raw, 'update-not-available'); - } - - @memoize - private get onRawUpdateAvailable(): Event<{ url: string; version: string; }> { - return filterEvent(fromNodeEventEmitter(this.raw, 'update-available', (_, url, version) => ({ url, version })), ({ url }) => !!url); - } - - @memoize - private get onRawUpdateDownloaded(): Event { - return fromNodeEventEmitter(this.raw, 'update-downloaded', (_, releaseNotes, version, date, url) => ({ releaseNotes, version, date })); - } - - get state(): State { - return this._state; - } - - set state(state: State) { - this._state = state; - this._onStateChange.fire(state); - } - - get availableUpdate(): IUpdate { - return this._availableUpdate; - } - - constructor( - @IRequestService requestService: IRequestService, - @ILifecycleService private lifecycleService: ILifecycleService, - @IConfigurationService private configurationService: IConfigurationService, - @ITelemetryService private telemetryService: ITelemetryService, - @IEnvironmentService private environmentService: IEnvironmentService, - @ILogService private logService: ILogService - ) { - if (process.platform === 'win32') { - this.raw = new Win32AutoUpdaterImpl(requestService); - } else if (process.platform === 'linux') { - this.raw = new LinuxAutoUpdaterImpl(requestService); - } else if (process.platform === 'darwin') { - this.raw = electron.autoUpdater; - } else { - return; - } - - if (this.environmentService.disableUpdates) { - return; - } - - const channel = this.getUpdateChannel(); - const feedUrl = this.getUpdateFeedUrl(channel); - - if (!feedUrl) { - return; // updates not available - } - - try { - this.raw.setFeedURL(feedUrl); - } catch (e) { - return; // application not signed - } - - this.state = State.Idle; - - // Start checking for updates after 30 seconds - this.scheduleCheckForUpdates(30 * 1000) - .done(null, err => this.logService.error(err)); - } - - private scheduleCheckForUpdates(delay = 60 * 60 * 1000): TPromise { - return TPromise.timeout(delay) - .then(() => this.checkForUpdates()) - .then(update => { - if (update) { - // Update found, no need to check more - return TPromise.as(null); - } - - // Check again after 1 hour - return this.scheduleCheckForUpdates(60 * 60 * 1000); - }); - } - - checkForUpdates(explicit = false): TPromise { - return this.throttler.queue(() => this._checkForUpdates(explicit)) - .then(null, err => { - if (explicit) { - this._onError.fire(err); - } - - return null; - }); - } - - private _checkForUpdates(explicit: boolean): TPromise { - if (this.state !== State.Idle) { - return TPromise.as(null); - } - - this._onCheckForUpdate.fire(); - this.state = State.CheckingForUpdate; - - const listeners: IDisposable[] = []; - const result = new TPromise((c, e) => { - once(this.onRawError)(e, null, listeners); - once(this.onRawUpdateNotAvailable)(() => c(null), null, listeners); - once(this.onRawUpdateAvailable)(({ url, version }) => url && c({ url, version }), null, listeners); - once(this.onRawUpdateDownloaded)(({ version, date, releaseNotes }) => c({ version, date, releaseNotes }), null, listeners); - - this.raw.checkForUpdates(); - }).then(update => { - if (!update) { - this._onUpdateNotAvailable.fire(explicit); - this.state = State.Idle; - /* __GDPR__ - "update:notAvailable" : { - "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:notAvailable', { explicit }); - - } else if (update.url) { - const data: IUpdate = { - url: update.url, - releaseNotes: '', - version: update.version, - date: new Date() - }; - - this._availableUpdate = data; - this._onUpdateAvailable.fire({ url: update.url, version: update.version }); - this.state = State.UpdateAvailable; - /* __GDPR__ - "update:available" : { - "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "currentVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:available', { explicit, version: update.version, currentVersion: product.commit }); - - } else { - const data: IRawUpdate = { - releaseNotes: update.releaseNotes, - version: update.version, - date: update.date - }; - - this._availableUpdate = data; - this._onUpdateReady.fire(data); - this.state = State.UpdateDownloaded; - /* __GDPR__ - "update:downloaded" : { - "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('update:downloaded', { version: update.version }); - } - - return update; - }, err => { - this.state = State.Idle; - return TPromise.wrapError(err); - }); - - return always(result, () => dispose(listeners)); - } - - private getUpdateChannel(): string { - const channel = this.configurationService.getValue('update.channel'); - return channel === 'none' ? null : product.quality; - } - - private getUpdateFeedUrl(channel: string): string { - if (!channel) { - return null; - } - - if (process.platform === 'win32' && !fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) { - return null; - } - - if (!product.updateUrl || !product.commit) { - return null; - } - - const platform = this.getUpdatePlatform(); - - return `${product.updateUrl}/api/update/${platform}/${channel}/${product.commit}`; - } - - private getUpdatePlatform(): string { - if (process.platform === 'linux') { - return `linux-${process.arch}`; - } - - if (process.platform === 'win32' && process.arch === 'x64') { - return 'win32-x64'; - } - - return process.platform; - } - - quitAndInstall(): TPromise { - if (!this._availableUpdate) { - return TPromise.as(null); - } - - if (this._availableUpdate.url) { - electron.shell.openExternal(this._availableUpdate.url); - return TPromise.as(null); - } - - this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); - - this.lifecycleService.quit(true /* from update */).done(vetod => { - this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); - if (vetod) { - return; - } - - // for some reason updating on Mac causes the local storage not to be flushed. - // we workaround this issue by forcing an explicit flush of the storage data. - // see also https://github.com/Microsoft/vscode/issues/172 - if (process.platform === 'darwin') { - this.logService.trace('update#quitAndInstall(): calling flushStorageData()'); - electron.session.defaultSession.flushStorageData(); - } - - this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); - this.raw.quitAndInstall(); - }); - - return TPromise.as(null); - } -} diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts new file mode 100644 index 0000000000000..b50b3ac33b6f1 --- /dev/null +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as fs from 'original-fs'; +import * as path from 'path'; +import * as pfs from 'vs/base/node/pfs'; +import { memoize } from 'vs/base/common/decorators'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; +import { IRequestService } from 'vs/platform/request/node/request'; +import product from 'vs/platform/node/product'; +import { TPromise, Promise } from 'vs/base/common/winjs.base'; +import { State, IUpdate, StateType } from 'vs/platform/update/common/update'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createUpdateURL, AbstractUpdateService } from 'vs/platform/update/electron-main/abstractUpdateService'; +import { download, asJson } from 'vs/base/node/request'; +import { checksum } from 'vs/base/node/crypto'; +import { tmpdir } from 'os'; +import { spawn } from 'child_process'; + +function pollUntil(fn: () => boolean, timeout = 1000): TPromise { + return new TPromise(c => { + const poll = () => { + if (fn()) { + c(null); + } else { + setTimeout(poll, timeout); + } + }; + + poll(); + }); +} + +interface IAvailableUpdate { + packagePath: string; + updateFilePath?: string; +} + +export class Win32UpdateService extends AbstractUpdateService { + + _serviceBrand: any; + + private url: string | undefined; + private availableUpdate: IAvailableUpdate | undefined; + + @memoize + get cachePath(): TPromise { + const result = path.join(tmpdir(), `vscode-update-${process.arch}`); + return pfs.mkdirp(result, null).then(() => result); + } + + constructor( + @ILifecycleService lifecycleService: ILifecycleService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private telemetryService: ITelemetryService, + @IEnvironmentService environmentService: IEnvironmentService, + @IRequestService private requestService: IRequestService, + @ILogService logService: ILogService + ) { + super(lifecycleService, configurationService, environmentService, logService); + } + + protected setUpdateFeedUrl(quality: string): boolean { + if (!fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) { + return false; + } + + this.url = createUpdateURL(process.arch === 'x64' ? 'win32-x64' : 'win32', quality); + return true; + } + + protected doCheckForUpdates(explicit: boolean): void { + if (!this.url) { + return; + } + + this.setState(State.CheckingForUpdates(explicit)); + + this.requestService.request({ url: this.url }) + .then(asJson) + .then(update => { + if (!update || !update.url || !update.version) { + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + + this.setState(State.Idle); + return TPromise.as(null); + } + + this.setState(State.Downloading(update)); + + return this.cleanup(update.version).then(() => { + return this.getUpdatePackagePath(update.version).then(updatePackagePath => { + return pfs.exists(updatePackagePath).then(exists => { + if (exists) { + return TPromise.as(updatePackagePath); + } + + const url = update.url; + const hash = update.hash; + const downloadPath = `${updatePackagePath}.tmp`; + + return this.requestService.request({ url }) + .then(context => download(downloadPath, context)) + .then(hash ? () => checksum(downloadPath, update.hash) : () => null) + .then(() => pfs.rename(downloadPath, updatePackagePath)) + .then(() => updatePackagePath); + }); + }).then(packagePath => { + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); + + this.availableUpdate = { packagePath }; + + if (fastUpdatesEnabled && update.supportsFastUpdate) { + this.setState(State.Downloaded(update)); + } else { + this.setState(State.Ready(update)); + } + }); + }); + }) + .then(null, err => { + this.logService.error(err); + /* __GDPR__ + "update:notAvailable" : { + "explicit" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('update:notAvailable', { explicit }); + this.setState(State.Idle); + }); + } + + private getUpdatePackagePath(version: string): TPromise { + return this.cachePath.then(cachePath => path.join(cachePath, `CodeSetup-${product.quality}-${version}.exe`)); + } + + private cleanup(exceptVersion: string = null): Promise { + const filter = exceptVersion ? one => !(new RegExp(`${product.quality}-${exceptVersion}\\.exe$`).test(one)) : () => true; + + return this.cachePath + .then(cachePath => pfs.readdir(cachePath) + .then(all => Promise.join(all + .filter(filter) + .map(one => pfs.unlink(path.join(cachePath, one)).then(null, () => null)) + )) + ); + } + + protected doApplyUpdate(): TPromise { + if (this.state.type !== StateType.Downloaded || !this.availableUpdate) { + return TPromise.as(null); + } + + const update = this.state.update; + this.setState(State.Updating(update)); + + return this.cachePath.then(cachePath => { + this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${product.quality}-${update.version}.flag`); + + return pfs.writeFile(this.availableUpdate.updateFilePath, 'flag').then(() => { + const child = spawn(this.availableUpdate.packagePath, ['/verysilent', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }); + + child.once('exit', () => { + this.availableUpdate = undefined; + this.setState(State.Idle); + }); + + const readyMutexName = `${product.win32MutexName}-ready`; + const isActive = (require.__$__nodeRequire('windows-mutex') as any).isActive; + + // poll for mutex-ready + pollUntil(() => isActive(readyMutexName)) + .then(() => this.setState(State.Ready(update))); + }); + }); + } + + protected doQuitAndInstall(): void { + if (this.state.type !== StateType.Ready) { + return; + } + + this.logService.trace('update#quitAndInstall(): running raw#quitAndInstall()'); + + if (this.state.update.supportsFastUpdate && this.availableUpdate.updateFilePath) { + fs.unlinkSync(this.availableUpdate.updateFilePath); + } else { + spawn(this.availableUpdate.packagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }); + } + } +} diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 627be4bee3400..122d34883f6d0 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -93,10 +93,10 @@ export class ActivitybarPart extends Part { return this.compositeBar.showActivity(viewletOrActionId, badge, clazz, priority); } - return this.showGlobalActivity(viewletOrActionId, badge); + return this.showGlobalActivity(viewletOrActionId, badge, clazz); } - private showGlobalActivity(globalActivityId: string, badge: IBadge): IDisposable { + private showGlobalActivity(globalActivityId: string, badge: IBadge, clazz?: string): IDisposable { if (!badge) { throw illegalArgument('badge'); } @@ -106,7 +106,7 @@ export class ActivitybarPart extends Part { throw illegalArgument('globalActivityId'); } - action.setBadge(badge); + action.setBadge(badge, clazz); return toDisposable(() => action.setBadge(undefined)); } diff --git a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts index 6c4f3102f8464..61b5c8d569798 100644 --- a/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositebar/compositeBarActions.ts @@ -12,7 +12,7 @@ import * as dom from 'vs/base/browser/dom'; import { Builder, $ } from 'vs/base/browser/builder'; import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, empty, toDisposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -53,6 +53,7 @@ export interface ICompositeBar { export class ActivityAction extends Action { private badge: IBadge; + private clazz: string | undefined; private _onDidChangeBadge = new Emitter(); constructor(private _activity: IActivity) { @@ -85,8 +86,13 @@ export class ActivityAction extends Action { return this.badge; } - public setBadge(badge: IBadge): void { + public getClass(): string | undefined { + return this.clazz; + } + + public setBadge(badge: IBadge, clazz?: string): void { this.badge = badge; + this.clazz = clazz; this._onDidChangeBadge.fire(this); } } @@ -110,6 +116,7 @@ export class ActivityActionItem extends BaseActionItem { protected options: IActivityActionItemOptions; private $badgeContent: Builder; + private badgeDisposable: IDisposable = empty; private mouseUpTimeout: number; constructor( @@ -199,7 +206,10 @@ export class ActivityActionItem extends BaseActionItem { this.updateStyles(); } - protected updateBadge(badge: IBadge): void { + protected updateBadge(badge: IBadge, clazz?: string): void { + this.badgeDisposable.dispose(); + this.badgeDisposable = empty; + this.$badgeContent.empty(); this.$badge.hide(); @@ -234,6 +244,11 @@ export class ActivityActionItem extends BaseActionItem { else if (badge instanceof ProgressBadge) { this.$badge.show(); } + + if (clazz) { + this.$badge.addClass(clazz); + this.badgeDisposable = toDisposable(() => this.$badge.removeClass(clazz)); + } } // Title @@ -259,7 +274,7 @@ export class ActivityActionItem extends BaseActionItem { private handleBadgeChangeEvenet(): void { const action = this.getAction(); if (action instanceof ActivityAction) { - this.updateBadge(action.getBadge()); + this.updateBadge(action.getBadge(), action.getClass()); } } diff --git a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts index d93239ac3ca6f..b533e60a755cb 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts @@ -57,6 +57,11 @@ configurationRegistry.registerConfiguration({ 'enum': ['none', 'default'], 'default': 'default', 'description': nls.localize('updateChannel', "Configure whether you receive automatic updates from an update channel. Requires a restart after change.") + }, + 'update.enableWindowsBackgroundUpdates': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates.") } } }); diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index a64d44eed6cf1..380a3c068ebbe 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -9,7 +9,6 @@ import nls = require('vs/nls'); import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction, Action } from 'vs/base/common/actions'; -import { mapEvent } from 'vs/base/common/event'; import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMessageService, CloseAction, Severity } from 'vs/platform/message/common/message'; @@ -17,7 +16,7 @@ import pkg from 'vs/platform/node/package'; import product from 'vs/platform/node/product'; import URI from 'vs/base/common/uri'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IActivityService, NumberBadge, IBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ReleaseNotesInput } from 'vs/workbench/parts/update/electron-browser/releaseNotesInput'; import { IGlobalActivity } from 'vs/workbench/common/activity'; @@ -29,21 +28,11 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IUpdateService, State as UpdateState } from 'vs/platform/update/common/update'; +import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; import * as semver from 'semver'; -import { OS, isLinux, isWindows } from 'vs/base/common/platform'; +import { OS } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -class ApplyUpdateAction extends Action { - constructor( @IUpdateService private updateService: IUpdateService) { - super('update.applyUpdate', nls.localize('updateNow', "Update Now"), null, true); - } - - run(): TPromise { - return this.updateService.quitAndInstall(); - } -} - const NotNowAction = new Action( 'update.later', nls.localize('later', "Later"), @@ -178,17 +167,6 @@ export class ShowCurrentReleaseNotesAction extends AbstractShowReleaseNotesActio } } -export class DownloadAction extends Action { - - constructor( @IUpdateService private updateService: IUpdateService) { - super('update.download', nls.localize('downloadNow', "Download Now"), null, true); - } - - run(): TPromise { - return this.updateService.quitAndInstall(); - } -} - const LinkAction = (id: string, message: string, licenseUrl: string) => new Action( id, message, null, true, () => { window.open(licenseUrl); return TPromise.as(null); } @@ -316,6 +294,7 @@ export class UpdateContribution implements IGlobalActivity { get name() { return ''; } get cssClass() { return 'update-activity'; } + private state: UpdateState; private badgeDisposable: IDisposable = EmptyDisposable; private disposables: IDisposable[] = []; @@ -328,13 +307,7 @@ export class UpdateContribution implements IGlobalActivity { @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IActivityService private activityService: IActivityService ) { - const onUpdateAvailable = isLinux - ? mapEvent(updateService.onUpdateAvailable, e => e.version) - : mapEvent(updateService.onUpdateReady, e => e.version); - - onUpdateAvailable(this.onUpdateAvailable, this, this.disposables); - updateService.onError(this.onError, this, this.disposables); - updateService.onUpdateNotAvailable(this.onUpdateNotAvailable, this, this.disposables); + this.state = updateService.state; updateService.onStateChange(this.onUpdateStateChange, this, this.disposables); this.onUpdateStateChange(this.updateService.state); @@ -358,74 +331,134 @@ export class UpdateContribution implements IGlobalActivity { } private onUpdateStateChange(state: UpdateState): void { - this.badgeDisposable.dispose(); + switch (state.type) { + case StateType.Idle: + if (this.state.type === StateType.CheckingForUpdates && this.state.explicit) { + this.onUpdateNotAvailable(); + } + break; + + case StateType.AvailableForDownload: + this.onUpdateAvailable(state.update); + break; - const isUpdateAvailable = isLinux - ? state === UpdateState.UpdateAvailable - : state === UpdateState.UpdateDownloaded; + case StateType.Downloaded: + this.onUpdateDownloaded(state.update); + break; - if (isUpdateAvailable) { - const badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); - this.badgeDisposable = this.activityService.showActivity(this.id, badge); + case StateType.Updating: + this.onUpdateUpdating(state.update); + break; + + case StateType.Ready: + this.onUpdateReady(state.update); + break; } - } - private onUpdateAvailable(version: string): void { - const currentVersion = product.commit; - const currentMillis = new Date().getTime(); - const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + let badge: IBadge | undefined = undefined; + let clazz: string | undefined; - // if version != stored version, save version and date - if (currentVersion !== lastKnownVersion) { - this.storageService.store('update/lastKnownVersion', currentVersion, StorageScope.GLOBAL); - this.storageService.store('update/updateNotificationTime', currentMillis, StorageScope.GLOBAL); + if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { + badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); + } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { + badge = new ProgressBadge(() => nls.localize('updateIsReady', "New {0} update available.", product.nameShort)); + clazz = 'progress-badge'; } - const updateNotificationMillis = this.storageService.getInteger('update/updateNotificationTime', StorageScope.GLOBAL, currentMillis); - const diffDays = (currentMillis - updateNotificationMillis) / (1000 * 60 * 60 * 24); + this.badgeDisposable.dispose(); - // if 5 days have passed from stored date, show message service - if (diffDays > 5) { - this.showUpdateNotification(version); + if (badge) { + this.badgeDisposable = this.activityService.showActivity(this.id, badge, clazz); } + + this.state = state; + } + + private onUpdateNotAvailable(): void { + this.messageService.show(severity.Info, nls.localize('noUpdatesAvailable', "There are no updates currently available.")); } - private showUpdateNotification(version: string): void { - const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, version); + // linux + private onUpdateAvailable(update: IUpdate): void { + if (!this.shouldShowNotification()) { + return; + } - if (isLinux) { - const downloadAction = this.instantiationService.createInstance(DownloadAction); + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + const downloadAction = new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => + this.updateService.downloadUpdate()); - this.messageService.show(severity.Info, { - message: nls.localize('thereIsUpdateAvailable', "There is an available update."), - actions: [downloadAction, NotNowAction, releaseNotesAction] - }); - } else { - const applyUpdateAction = this.instantiationService.createInstance(ApplyUpdateAction); + this.messageService.show(severity.Info, { + message: nls.localize('thereIsUpdateAvailable', "There is an available update."), + actions: [downloadAction, NotNowAction, releaseNotesAction] + }); + } - this.messageService.show(severity.Info, { - message: nls.localize('updateAvailable', "{0} will be updated after it restarts.", product.nameLong), - actions: [applyUpdateAction, NotNowAction, releaseNotesAction] - }); + // windows fast updates + private onUpdateDownloaded(update: IUpdate): void { + if (!this.shouldShowNotification()) { + return; + } + + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + const installUpdateAction = new Action('update.applyUpdate', nls.localize('installUpdate', "Install Update"), undefined, true, () => + this.updateService.applyUpdate()); + + this.messageService.show(severity.Info, { + message: nls.localize('updateAvailable', "There's an available update: {0} {1}", product.nameLong, update.productVersion), + actions: [installUpdateAction, NotNowAction, releaseNotesAction] + }); + } + + // windows fast updates + private onUpdateUpdating(update: IUpdate): void { + const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); + + if (!neverShowAgain.shouldShow()) { + return; } + + this.messageService.show(severity.Info, { + message: nls.localize('updateInstalling', "{0} {1} is being installed in the background, we'll let you know when it's done.", product.nameLong, update.productVersion), + actions: [CloseAction, neverShowAgain.action] + }); } - private onUpdateNotAvailable(explicit: boolean): void { - if (!explicit) { + // windows and mac + private onUpdateReady(update: IUpdate): void { + if (!this.shouldShowNotification()) { return; } - this.messageService.show(severity.Info, nls.localize('noUpdatesAvailable', "There are no updates currently available.")); + const releaseNotesAction = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + const applyUpdateAction = new Action('update.applyUpdate', nls.localize('updateNow', "Update Now"), undefined, true, () => + this.updateService.quitAndInstall()); + + this.messageService.show(severity.Info, { + message: nls.localize('updateAvailableAfterRestart', "{0} will be updated after it restarts.", product.nameLong), + actions: [applyUpdateAction, NotNowAction, releaseNotesAction] + }); } - private onError(err: any): void { - this.messageService.show(severity.Error, err); + private shouldShowNotification(): boolean { + const currentVersion = product.commit; + const currentMillis = new Date().getTime(); + const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + + // if version != stored version, save version and date + if (currentVersion !== lastKnownVersion) { + this.storageService.store('update/lastKnownVersion', currentVersion, StorageScope.GLOBAL); + this.storageService.store('update/updateNotificationTime', currentMillis, StorageScope.GLOBAL); + } + + const updateNotificationMillis = this.storageService.getInteger('update/updateNotificationTime', StorageScope.GLOBAL, currentMillis); + const diffDays = (currentMillis - updateNotificationMillis) / (1000 * 60 * 60 * 24); + + return diffDays > 5; } getActions(): IAction[] { - const updateAction = this.getUpdateAction(); - - return [ + const result: IAction[] = [ new CommandAction(UpdateContribution.showCommandsId, nls.localize('commandPalette', "Command Palette..."), this.commandService), new Separator(), new CommandAction(UpdateContribution.openSettingsId, nls.localize('settings', "Settings"), this.commandService), @@ -434,39 +467,49 @@ export class UpdateContribution implements IGlobalActivity { new CommandAction(UpdateContribution.openUserSnippets, nls.localize('userSnippets', "User Snippets"), this.commandService), new Separator(), new CommandAction(UpdateContribution.selectColorThemeId, nls.localize('selectTheme.label', "Color Theme"), this.commandService), - new CommandAction(UpdateContribution.selectIconThemeId, nls.localize('themes.selectIconTheme.label', "File Icon Theme"), this.commandService), - new Separator(), - updateAction + new CommandAction(UpdateContribution.selectIconThemeId, nls.localize('themes.selectIconTheme.label', "File Icon Theme"), this.commandService) ]; + + const updateAction = this.getUpdateAction(); + + if (updateAction) { + result.push(new Separator(), updateAction); + } + + return result; } - private getUpdateAction(): IAction { - switch (this.updateService.state) { - case UpdateState.Uninitialized: - return new Action('update.notavailable', nls.localize('not available', "Updates Not Available"), undefined, false); + private getUpdateAction(): IAction | null { + const state = this.updateService.state; + + switch (state.type) { + case StateType.Uninitialized: + return null; + + case StateType.Idle: + return new Action('update.check', nls.localize('checkForUpdates', "Check for Updates..."), undefined, true, () => + this.updateService.checkForUpdates(true)); - case UpdateState.CheckingForUpdate: + case StateType.CheckingForUpdates: return new Action('update.checking', nls.localize('checkingForUpdates', "Checking For Updates..."), undefined, false); - case UpdateState.UpdateAvailable: - if (isLinux) { - return new Action('update.linux.available', nls.localize('DownloadUpdate', "Download Available Update"), undefined, true, () => - this.updateService.quitAndInstall()); - } + case StateType.AvailableForDownload: + return new Action('update.downloadNow', nls.localize('download now', "Download Now"), null, true, () => + this.updateService.downloadUpdate()); + + case StateType.Downloading: + return new Action('update.downloading', nls.localize('DownloadingUpdate', "Downloading Update..."), undefined, false); - const updateAvailableLabel = isWindows - ? nls.localize('DownloadingUpdate', "Downloading Update...") - : nls.localize('InstallingUpdate', "Installing Update..."); + case StateType.Downloaded: + return new Action('update.install', nls.localize('installUpdate...', "Install Update..."), undefined, true, () => + this.updateService.applyUpdate()); - return new Action('update.available', updateAvailableLabel, undefined, false); + case StateType.Updating: + return new Action('update.updating', nls.localize('installingUpdate', "Installing Update..."), undefined, false); - case UpdateState.UpdateDownloaded: + case StateType.Ready: return new Action('update.restart', nls.localize('restartToUpdate', "Restart to Update..."), undefined, true, () => this.updateService.quitAndInstall()); - - default: - return new Action('update.check', nls.localize('checkForUpdates', "Check for Updates..."), undefined, this.updateService.state === UpdateState.Idle, () => - this.updateService.checkForUpdates(true)); } }