From f5a6f14aded7dbc22cee6ee7026c171ccc9e6804 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 7 Aug 2019 16:18:17 -0500 Subject: [PATCH] Implement update service --- scripts/tasks.bash | 2 +- scripts/vscode.patch | 449 ++++++++++++++++++++++++++++++++++++++++++- src/cli.ts | 217 +++++++++++++-------- src/ipc.ts | 53 +++++ src/server.ts | 13 +- src/update.ts | 136 +++++++++++++ 6 files changed, 786 insertions(+), 84 deletions(-) create mode 100644 src/ipc.ts create mode 100644 src/update.ts diff --git a/scripts/tasks.bash b/scripts/tasks.bash index fbd3f2ff..9b63b5e2 100755 --- a/scripts/tasks.bash +++ b/scripts/tasks.bash @@ -74,7 +74,7 @@ function build-code-server() { cd "${buildPath}" && yarn --production --force --build-from-source rm "${buildPath}/"{package.json,yarn.lock,.yarnrc} - local packageJson="{\"codeServerVersion\": \"${codeServerVersion}\"}" + local packageJson="{\"codeServerVersion\": \"${codeServerVersion}-vsc${vscodeVersion}\"}" cp -r "${sourcePath}/.build/extensions" "${buildPath}" node "${rootPath}/scripts/merge.js" "${sourcePath}/package.json" "${rootPath}/scripts/package.json" "${buildPath}/package.json" "${packageJson}" node "${rootPath}/scripts/merge.js" "${sourcePath}/.build/product.json" "${rootPath}/scripts/product.json" "${buildPath}/product.json" diff --git a/scripts/vscode.patch b/scripts/vscode.patch index 6b623c94..f9a9192c 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -593,6 +593,31 @@ index 9f68b645b6..f0cae7111d 100644 setLevel(level: LogLevel): void { this.channel.call('setLevel', level); } +diff --git a/src/vs/platform/product/browser/productService.ts b/src/vs/platform/product/browser/productService.ts +index 084144d009..d91176c9cd 100644 +--- a/src/vs/platform/product/browser/productService.ts ++++ b/src/vs/platform/product/browser/productService.ts +@@ -17,7 +17,7 @@ export class ProductService implements IProductService { + + _serviceBrand: ServiceIdentifier; + +- get version(): string { return '1.35.0'; } ++ get version(): string { return this.productConfiguration ? (this.productConfiguration as any).version : 'development'; } + + get commit(): string | undefined { return undefined; } + +diff --git a/src/vs/platform/product/node/package.ts b/src/vs/platform/product/node/package.ts +index d39c5877d6..c189d6f19f 100644 +--- a/src/vs/platform/product/node/package.ts ++++ b/src/vs/platform/product/node/package.ts +@@ -9,6 +9,7 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; + export interface IPackageConfiguration { + name: string; + version: string; ++ codeServerVersion: string; + } + + const rootPath = path.dirname(getPathFromAmdModule(require, '')); diff --git a/src/vs/platform/remote/browser/browserWebSocketFactory.ts b/src/vs/platform/remote/browser/browserWebSocketFactory.ts index 6d9ecbcf5a..1b3499dddf 100644 --- a/src/vs/platform/remote/browser/browserWebSocketFactory.ts @@ -673,6 +698,98 @@ index 8e1b68eb36..2b6a0d5b15 100644 + return true; + } +} +diff --git a/src/vs/platform/update/electron-browser/updateService.ts b/src/vs/platform/update/electron-browser/updateService.ts +index 023ea3ea7d..455df76ee3 100644 +--- a/src/vs/platform/update/electron-browser/updateService.ts ++++ b/src/vs/platform/update/electron-browser/updateService.ts +@@ -6,8 +6,9 @@ + import { IChannel } from 'vs/base/parts/ipc/common/ipc'; + import { Event, Emitter } from 'vs/base/common/event'; + import { IUpdateService, State } from 'vs/platform/update/common/update'; +-import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; ++// import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; + import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; ++import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; + + export class UpdateService implements IUpdateService { + +@@ -21,8 +22,11 @@ export class UpdateService implements IUpdateService { + + private channel: IChannel; + +- constructor(@IMainProcessService mainProcessService: IMainProcessService) { +- this.channel = mainProcessService.getChannel('update'); ++ constructor( ++ // @IMainProcessService mainProcessService: IMainProcessService ++ @IRemoteAgentService remoteAgentService: IRemoteAgentService ++ ) { ++ this.channel = remoteAgentService.getConnection()!.getChannel('update'); + + // always set this._state as the state changes + this.onStateChange(state => this._state = state); +diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts +index fcca3cbfb9..101f3d33c9 100644 +--- a/src/vs/platform/update/electron-main/abstractUpdateService.ts ++++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts +@@ -6,7 +6,7 @@ + import { Event, Emitter } from 'vs/base/common/event'; + import { timeout } from 'vs/base/common/async'; + import { IConfigurationService, getMigratedSettingValue } from 'vs/platform/configuration/common/configuration'; +-import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; ++// import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; + import product from 'vs/platform/product/node/product'; + import { IUpdateService, State, StateType, AvailableForDownload, UpdateType } from 'vs/platform/update/common/update'; + import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +@@ -40,7 +40,8 @@ export abstract class AbstractUpdateService implements IUpdateService { + } + + constructor( +- @ILifecycleService private readonly lifecycleService: ILifecycleService, ++ _placeholder: any, // To prevent errors from the extending classes. ++ // @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IConfigurationService protected configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IRequestService protected requestService: IRequestService, +@@ -51,7 +52,7 @@ export abstract class AbstractUpdateService implements IUpdateService { + return; + } + +- if (!product.updateUrl || !product.commit) { ++ if (/*!product.updateUrl || */!product.commit) { + this.logService.info('update#ctor - updates are disabled as there is no update URL'); + return; + } +@@ -82,7 +83,7 @@ export abstract class AbstractUpdateService implements IUpdateService { + } + + private getProductQuality(updateMode: string): string | undefined { +- return updateMode === 'none' ? undefined : product.quality; ++ return updateMode === 'none' ? undefined : 'quality'; + } + + private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { +@@ -141,15 +142,15 @@ export abstract class AbstractUpdateService implements IUpdateService { + + this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); + +- this.lifecycleService.quit(true /* from update */).then(vetod => { +- this.logService.trace(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); +- if (vetod) { +- return; +- } ++ // this.quit(true /* from update */).then(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 Promise.resolve(undefined); + } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 8bace46843..b261f40493 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -801,7 +918,7 @@ index 1986fb6642..70b0c789e3 100644 const payload = await this.resolveWorkspaceInitializationPayload(); diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts -index b253e573ae..94b2b7f287 100644 +index b253e573ae..164e50c3d1 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -53,6 +53,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur @@ -910,6 +1027,15 @@ index b253e573ae..94b2b7f287 100644 //#endregion +@@ -714,7 +750,7 @@ export class SimpleUpdateService implements IUpdateService { + } + } + +-registerSingleton(IUpdateService, SimpleUpdateService); ++// registerSingleton(IUpdateService, SimpleUpdateService); + + //#endregion + @@ -888,7 +924,7 @@ export class SimpleWindowService extends Disposable implements IWindowService { for (let i = 0; i < _uris.length; i++) { const uri = _uris[i]; @@ -1470,6 +1596,297 @@ index 4d8a5d6907..b464d5276f 100644 template.decorationIcon.title = resource.decorations.tooltip || ''; } else { template.decorationIcon.style.display = 'none'; +diff --git a/src/vs/workbench/contrib/update/electron-browser/update.contribution.ts b/src/vs/workbench/contrib/update/electron-browser/update.contribution.ts +index e39fa57979..d0548847a4 100644 +--- a/src/vs/workbench/contrib/update/electron-browser/update.contribution.ts ++++ b/src/vs/workbench/contrib/update/electron-browser/update.contribution.ts +@@ -4,26 +4,26 @@ + *--------------------------------------------------------------------------------------------*/ + + import 'vs/platform/update/node/update.config.contribution'; +-import * as platform from 'vs/base/common/platform'; ++// import * as platform from 'vs/base/common/platform'; + import { Registry } from 'vs/platform/registry/common/platform'; + import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +-import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; +-import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +-import { ShowCurrentReleaseNotesAction, ProductContribution, UpdateContribution, Win3264BitContribution } from './update'; ++// import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; ++// import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; ++import { /*ShowCurrentReleaseNotesAction, ProductContribution, */UpdateContribution/*, Win3264BitContribution */} from './update'; + import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + + const workbench = Registry.as(WorkbenchExtensions.Workbench); + +-workbench.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Restored); ++/* workbench.registerWorkbenchContribution(ProductContribution, LifecyclePhase.Restored); + + if (platform.isWindows) { + if (process.arch === 'ia32') { + workbench.registerWorkbenchContribution(Win3264BitContribution, LifecyclePhase.Restored); + } +-} ++} */ + + workbench.registerWorkbenchContribution(UpdateContribution, LifecyclePhase.Restored); + + // Editor +-Registry.as(ActionExtensions.WorkbenchActions) +- .registerWorkbenchAction(new SyncActionDescriptor(ShowCurrentReleaseNotesAction, ShowCurrentReleaseNotesAction.ID, ShowCurrentReleaseNotesAction.LABEL), 'Show Release Notes'); ++// Registry.as(ActionExtensions.WorkbenchActions) ++// .registerWorkbenchAction(new SyncActionDescriptor(ShowCurrentReleaseNotesAction, ShowCurrentReleaseNotesAction.ID, ShowCurrentReleaseNotesAction.LABEL), 'Show Release Notes'); +diff --git a/src/vs/workbench/contrib/update/electron-browser/update.ts b/src/vs/workbench/contrib/update/electron-browser/update.ts +index 7078a1bb2f..94b1ce31a7 100644 +--- a/src/vs/workbench/contrib/update/electron-browser/update.ts ++++ b/src/vs/workbench/contrib/update/electron-browser/update.ts +@@ -7,32 +7,33 @@ import * as nls from 'vs/nls'; + import severity from 'vs/base/common/severity'; + import { Action } from 'vs/base/common/actions'; + import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +-import pkg from 'vs/platform/product/node/package'; +-import product from 'vs/platform/product/node/product'; +-import { URI } from 'vs/base/common/uri'; ++// import pkg from 'vs/platform/product/node/package'; ++// import product from 'vs/platform/product/node/product'; ++// import { URI } from 'vs/base/common/uri'; + import { IActivityService, NumberBadge, IBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; +-import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; ++// import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; +-import { IOpenerService } from 'vs/platform/opener/common/opener'; ++// import { IOpenerService } from 'vs/platform/opener/common/opener'; + import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; + import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; + import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; +-import * as semver from 'semver'; +-import { IEnvironmentService } from 'vs/platform/environment/common/environment'; ++// import * as semver from 'semver'; ++// import { IEnvironmentService } from 'vs/platform/environment/common/environment'; + import { INotificationService, INotificationHandle, Severity } from 'vs/platform/notification/common/notification'; + import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +-import { ReleaseNotesManager } from './releaseNotesEditor'; +-import { isWindows } from 'vs/base/common/platform'; +-import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; ++// import { ReleaseNotesManager } from './releaseNotesEditor'; ++// import { isWindows } from 'vs/base/common/platform'; ++// import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; + import { CommandsRegistry } from 'vs/platform/commands/common/commands'; + import { FalseContext } from 'vs/platform/contextkey/common/contextkeys'; ++import { IProductService } from 'vs/platform/product/common/product'; + + const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Uninitialized); + +-let releaseNotesManager: ReleaseNotesManager | undefined = undefined; ++/*let releaseNotesManager: ReleaseNotesManager | undefined = undefined; + + function showReleaseNotes(instantiationService: IInstantiationService, version: string) { + if (!releaseNotesManager) { +@@ -150,7 +151,7 @@ export class ProductContribution implements IWorkbenchContribution { + + storageService.store(ProductContribution.KEY, pkg.version, StorageScope.GLOBAL); + } +-} ++} */ + + class NeverShowAgain { + +@@ -175,7 +176,7 @@ class NeverShowAgain { + } + } + +-export class Win3264BitContribution implements IWorkbenchContribution { ++/* export class Win3264BitContribution implements IWorkbenchContribution { + + private static readonly KEY = 'update/win32-64bits'; + private static readonly URL = 'https://code.visualstudio.com/updates/v1_15#_windows-64-bit'; +@@ -214,7 +215,7 @@ export class Win3264BitContribution implements IWorkbenchContribution { + { sticky: true } + ); + } +-} ++} */ + + export class UpdateContribution extends Disposable implements IWorkbenchContribution { + +@@ -224,7 +225,8 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + + constructor( + @IStorageService private readonly storageService: IStorageService, +- @IInstantiationService private readonly instantiationService: IInstantiationService, ++ // @IInstantiationService private readonly instantiationService: IInstantiationService, ++ @IProductService private readonly productService: IProductService, + @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogService: IDialogService, + @IUpdateService private readonly updateService: IUpdateService, +@@ -247,7 +249,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + updated since 5 days. + */ + +- const currentVersion = product.commit; ++ const currentVersion = this.productService.commit; + const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + + // if current version != stored version, clear both fields +@@ -292,9 +294,9 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + let clazz: string | undefined; + + 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)); ++ badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameLong)); + } 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)); ++ badge = new ProgressBadge(() => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameLong)); + clazz = 'progress-badge'; + } + +@@ -333,21 +335,21 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + + this.notificationService.prompt( + severity.Info, +- nls.localize('thereIsUpdateAvailable', "There is an available update."), ++ nls.localize('updateAvailable', "There's an update available: {0} {1}", this.productService.nameLong, update.productVersion), + [{ +- label: nls.localize('download update', "Download Update"), ++ label: nls.localize('installUpdate', "Install Update"), + run: () => this.updateService.downloadUpdate() + }, { + label: nls.localize('later', "Later"), + run: () => { } +- }, { ++ }/*, { + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const action = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + action.run(); + action.dispose(); + } +- }], ++ }*/], + { sticky: true } + ); + } +@@ -360,30 +362,30 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + + this.notificationService.prompt( + severity.Info, +- nls.localize('updateAvailable', "There's an update available: {0} {1}", product.nameLong, update.productVersion), ++ nls.localize('updateAvailable', "There's an update available: {0} {1}", this.productService.nameLong, update.productVersion), + [{ + label: nls.localize('installUpdate', "Install Update"), + run: () => this.updateService.applyUpdate() + }, { + label: nls.localize('later', "Later"), + run: () => { } +- }, { ++ }/*, { + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + const action = this.instantiationService.createInstance(ShowReleaseNotesAction, update.productVersion); + action.run(); + action.dispose(); + } +- }], ++ }*/], + { sticky: true } + ); + } + + // windows fast updates + private onUpdateUpdating(update: IUpdate): void { +- if (isWindows && product.target === 'user') { +- return; +- } ++ // if (isWindows && product.target === 'user') { ++ // return; ++ // } + + // windows fast updates (target === system) + const neverShowAgain = new NeverShowAgain('update/win32-fast-updates', this.storageService); +@@ -394,7 +396,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + + const handle = this.notificationService.prompt( + severity.Info, +- nls.localize('updateInstalling', "{0} {1} is being installed in the background; we'll let you know when it's done.", product.nameLong, update.productVersion), ++ nls.localize('updateInstalling', "{0} {1} is being installed in the background; we'll let you know when it's done.", this.productService.nameLong, update.productVersion), + [{ + label: nls.localize('neveragain', "Don't Show Again"), + isSecondary: true, +@@ -408,20 +410,23 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + + // windows and mac + private onUpdateReady(update: IUpdate): void { +- if (!(isWindows && product.target !== 'user') && !this.shouldShowNotification()) { +- return; +- } ++ // if (!(isWindows && product.target !== 'user') && !this.shouldShowNotification()) { ++ // return; ++ // } + + const actions = [{ + label: nls.localize('updateNow', "Update Now"), +- run: () => this.updateService.quitAndInstall() ++ run: () => { ++ this.updateService.quitAndInstall(); ++ window.location.reload(); ++ } + }, { + label: nls.localize('later', "Later"), + run: () => { } + }]; + + // TODO@joao check why snap updates send `update` as falsy +- if (update.productVersion) { ++ /*if (update.productVersion) { + actions.push({ + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { +@@ -430,19 +435,19 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + action.dispose(); + } + }); +- } ++ }*/ + + // windows user fast updates and mac + this.notificationService.prompt( + severity.Info, +- nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", product.nameLong), ++ nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), + actions, + { sticky: true } + ); + } + + private shouldShowNotification(): boolean { +- const currentVersion = product.commit; ++ const currentVersion = this.productService.commit; + const currentMillis = new Date().getTime(); + const lastKnownVersion = this.storageService.get('update/lastKnownVersion', StorageScope.GLOBAL); + +@@ -485,7 +490,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + group: '5_update', + command: { + id: 'update.downloadNow', +- title: nls.localize('download update', "Download Update") ++ title: nls.localize('installUpdate...', "Install Update...") + }, + when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload) + }); +@@ -522,7 +527,10 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu + when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.Updating) + }); + +- CommandsRegistry.registerCommand('update.restart', () => this.updateService.quitAndInstall()); ++ CommandsRegistry.registerCommand('update.restart', () => { ++ this.updateService.quitAndInstall(); ++ window.location.reload(); ++ }); + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_update', + command: { diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index a6be033e07..a4dcb7357a 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -1620,10 +2037,10 @@ index 306d58f915..58c603ad3d 100644 if (definition.fontCharacter || definition.fontColor) { let body = ''; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index c28adc0ad9..f76612a4d7 100644 +index c28adc0ad9..56c86d7e39 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts -@@ -72,8 +72,8 @@ import { BrowserLifecycleService } from 'vs/platform/lifecycle/browser/lifecycle +@@ -72,15 +72,15 @@ import { BrowserLifecycleService } from 'vs/platform/lifecycle/browser/lifecycle import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { DialogService } from 'vs/platform/dialogs/browser/dialogService'; @@ -1634,6 +2051,15 @@ index c28adc0ad9..f76612a4d7 100644 // import { ISharedProcessService, SharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; // import { IProductService } from 'vs/platform/product/common/product'; // import { ProductService } from 'vs/platform/product/node/productService'; + // import { IWindowsService } from 'vs/platform/windows/common/windows'; + // import { WindowsService } from 'vs/platform/windows/electron-browser/windowsService'; +-// import { IUpdateService } from 'vs/platform/update/common/update'; +-// import { UpdateService } from 'vs/platform/update/electron-browser/updateService'; ++import { IUpdateService } from 'vs/platform/update/common/update'; ++import { UpdateService } from 'vs/platform/update/electron-browser/updateService'; + // import { IIssueService } from 'vs/platform/issue/common/issue'; + // import { IssueService } from 'vs/platform/issue/electron-browser/issueService'; + // import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; @@ -128,7 +128,7 @@ import 'vs/workbench/services/extensions/browser/extensionService'; // import 'vs/workbench/services/contextmenu/electron-browser/contextmenuService'; // import 'vs/workbench/services/extensions/node/multiExtensionManagement'; @@ -1643,7 +2069,7 @@ index c28adc0ad9..f76612a4d7 100644 // import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import 'vs/workbench/services/notification/common/notificationService'; // import 'vs/workbench/services/window/electron-browser/windowService'; -@@ -156,7 +156,7 @@ registerSingleton(IContextViewService, ContextViewService, true); +@@ -156,10 +156,10 @@ registerSingleton(IContextViewService, ContextViewService, true); // registerSingleton(IExtensionGalleryService, ExtensionGalleryService, true); // registerSingleton(IRequestService, RequestService, true); registerSingleton(ILifecycleService, BrowserLifecycleService); @@ -1651,7 +2077,11 @@ index c28adc0ad9..f76612a4d7 100644 +registerSingleton(ILocalizationsService, LocalizationsService); // registerSingleton(ISharedProcessService, SharedProcessService, true); // registerSingleton(IWindowsService, WindowsService); - // registerSingleton(IUpdateService, UpdateService); +-// registerSingleton(IUpdateService, UpdateService); ++registerSingleton(IUpdateService, UpdateService); + // registerSingleton(IIssueService, IssueService); + // registerSingleton(IWorkspacesService, WorkspacesService); + // registerSingleton(IMenubarService, MenubarService); @@ -194,7 +194,7 @@ import 'vs/workbench/services/files/common/workspaceWatcher'; import 'vs/workbench/contrib/telemetry/browser/telemetry.contribution'; @@ -1674,3 +2104,12 @@ index c28adc0ad9..f76612a4d7 100644 // Output Panel import 'vs/workbench/contrib/output/browser/output.contribution'; +@@ -318,7 +318,7 @@ import 'vs/workbench/contrib/format/browser/format.contribution'; + // import 'vs/workbench/contrib/feedback/browser/feedback.contribution'; + + // Update +-// import 'vs/workbench/contrib/update/electron-browser/update.contribution'; ++import 'vs/workbench/contrib/update/electron-browser/update.contribution'; + + // Surveys + // import 'vs/workbench/contrib/surveys/electron-browser/nps.contribution'; diff --git a/src/cli.ts b/src/cli.ts index 8f0bd528..6dc72b53 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,16 +1,27 @@ +import * as cp from "child_process"; import * as os from "os"; +import { main as vsCli } from "vs/code/node/cliProcessMain"; import { validatePaths } from "vs/code/node/paths"; import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper"; -import { ParsedArgs } from "vs/platform/environment/common/environment"; import { buildHelpMessage, buildVersionMessage, options } from "vs/platform/environment/node/argv"; +import { ParsedArgs } from "vs/platform/environment/common/environment"; import pkg from "vs/platform/product/node/package"; import product from "vs/platform/product/node/product"; +import { ipcMain } from "vs/server/src/ipc"; + +product.extensionsGallery = { + serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com", + itemUrl: process.env.ITEM_URL || "", + controlUrl: "", + recommendationsUrl: "", + ...(product.extensionsGallery || {}), +}; + import { MainServer } from "vs/server/src/server"; import { enableExtensionTars } from "vs/server/src/tar"; import { AuthType, buildAllowedMessage, generateCertificate, generatePassword, localRequire, open, unpackExecutables } from "vs/server/src/util"; -import { main as vsCli } from "vs/code/node/cliProcessMain"; const { logger } = localRequire("@coder/logger/out/index"); @@ -27,88 +38,57 @@ interface Args extends ParsedArgs { socket?: string; } -// The last item is _ which is like -- so our options need to come before it. -const last = options.pop()!; +const getArgs = (): Args => { + // The last item is _ which is like -- so our options need to come before it. + const last = options.pop()!; -// Remove options that won't work or don't make sense. -let i = options.length; -while (i--) { - switch (options[i].id) { - case "add": - case "diff": - case "file-uri": - case "folder-uri": - case "goto": - case "new-window": - case "reuse-window": - case "wait": - case "disable-gpu": - // TODO: pretty sure these don't work but not 100%. - case "max-memory": - case "prof-startup": - case "inspect-extensions": - case "inspect-brk-extensions": - options.splice(i, 1); - break; + // Remove options that won't work or don't make sense. + let i = options.length; + while (i--) { + switch (options[i].id) { + case "add": + case "diff": + case "file-uri": + case "folder-uri": + case "goto": + case "new-window": + case "reuse-window": + case "wait": + case "disable-gpu": + // TODO: pretty sure these don't work but not 100%. + case "max-memory": + case "prof-startup": + case "inspect-extensions": + case "inspect-brk-extensions": + options.splice(i, 1); + break; + } } -} -options.push({ id: "base-path", type: "string", cat: "o", description: "Base path of the URL at which code-server is hosted (used for login redirects)." }); -options.push({ id: "cert", type: "string", cat: "o", description: "Path to certificate. If the path is omitted, both this and --cert-key will be generated." }); -options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to the certificate's key if one was provided." }); -options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to an extra builtin extension directory." }); -options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to an extra user extension directory." }); -options.push({ id: "host", type: "string", cat: "o", description: "Host for the server." }); -options.push({ id: "auth", type: "string", cat: "o", description: `The type of authentication to use. ${buildAllowedMessage(AuthType)}.` }); -options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." }); -options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." }); -options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." }); + options.push({ id: "base-path", type: "string", cat: "o", description: "Base path of the URL at which code-server is hosted (used for login redirects)." }); + options.push({ id: "cert", type: "string", cat: "o", description: "Path to certificate. If the path is omitted, both this and --cert-key will be generated." }); + options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to the certificate's key if one was provided." }); + options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to an extra builtin extension directory." }); + options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to an extra user extension directory." }); + options.push({ id: "host", type: "string", cat: "o", description: "Host for the server." }); + options.push({ id: "auth", type: "string", cat: "o", description: `The type of authentication to use. ${buildAllowedMessage(AuthType)}.` }); + options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." }); + options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." }); + options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." }); -options.push(last); + options.push(last); -const main = async (): Promise => { const args = validatePaths(parseMainProcessArgv(process.argv)) as Args; ["extra-extensions-dir", "extra-builtin-extensions-dir"].forEach((key) => { if (typeof args[key] === "string") { args[key] = [args[key]]; } }); + return args; +}; - if (!product.extensionsGallery) { - product.extensionsGallery = { - serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com", - itemUrl: process.env.ITEM_URL || "", - controlUrl: "", - recommendationsUrl: "", - }; - } - - const version = `${(pkg as any).codeServerVersion || "development"}-vsc${pkg.version}`; - if (args.help) { - const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`; - return console.log(buildHelpMessage(product.nameLong, executable, version, undefined, false)); - } - - if (args.version) { - return buildVersionMessage(version, product.commit).split("\n").map((line) => logger.info(line)); - } - - enableExtensionTars(); - - const shouldSpawnCliProcess = (): boolean => { - return !!args["install-source"] - || !!args["list-extensions"] - || !!args["install-extension"] - || !!args["uninstall-extension"] - || !!args["locate-extension"] - || !!args["telemetry"]; - }; - - if (shouldSpawnCliProcess()) { - await vsCli(args); - return process.exit(0); // There is a WriteStream instance keeping it open. - } - +const startVscode = async (): Promise => { + const args = getArgs(); const extra = args["_"] || []; const options = { auth: args.auth, @@ -136,6 +116,8 @@ const main = async (): Promise => { options.certKey = certKey; } + enableExtensionTars(); + const server = new MainServer({ ...options, port: typeof args.port !== "undefined" && parseInt(args.port, 10) || 8443, @@ -168,14 +150,99 @@ const main = async (): Promise => { } if (!server.options.socket && args.open) { - // The web socket doesn't seem to work if using 0.0.0.0. + // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = `http://localhost:${server.options.port}`; await open(openAddress).catch(console.error); logger.info(` - Opened ${openAddress}`); } }; +const startCli = (): boolean | Promise => { + const args = getArgs(); + if (args.help) { + const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`; + console.log(buildHelpMessage(product.nameLong, executable, pkg.codeServerVersion, undefined, false)); + return true; + } + + if (args.version) { + buildVersionMessage(pkg.codeServerVersion, product.commit).split("\n").map((line) => logger.info(line)); + return true; + } + + const shouldSpawnCliProcess = (): boolean => { + return !!args["install-source"] + || !!args["list-extensions"] + || !!args["install-extension"] + || !!args["uninstall-extension"] + || !!args["locate-extension"] + || !!args["telemetry"]; + }; + + if (shouldSpawnCliProcess()) { + enableExtensionTars(); + return vsCli(args); + } + + return false; +}; + +export class WrapperProcess { + private process?: cp.ChildProcess; + private started?: Promise; + + public constructor() { + ipcMain.onMessage(async (message) => { + switch (message) { + case "relaunch": + logger.info("Relaunching..."); + this.started = undefined; + if (this.process) { + this.process.kill(); + } + try { + await this.start(); + } catch (error) { + logger.error(error.message); + process.exit(typeof error.code === "number" ? error.code : 1); + } + break; + default: + logger.error(`Unrecognized message ${message}`); + break; + } + }); + } + + public start(): Promise { + if (!this.started) { + const child = this.spawn(); + this.started = ipcMain.handshake(child); + this.process = child; + } + return this.started; + } + + private spawn(): cp.ChildProcess { + return cp.spawn(process.argv[0], process.argv.slice(1), { + env: { + ...process.env, + LAUNCH_VSCODE: "true", + }, + stdio: ["inherit", "inherit", "inherit", "ipc"], + }); + } +} + +const main = async(): Promise => { + if (process.env.LAUNCH_VSCODE) { + await ipcMain.handshake(); + return startVscode(); + } + return startCli() || new WrapperProcess().start(); +}; + main().catch((error) => { - console.error(error); - process.exit(1); + logger.error(error.message); + process.exit(typeof error.code === "number" ? error.code : 1); }); diff --git a/src/ipc.ts b/src/ipc.ts new file mode 100644 index 00000000..48e85fe4 --- /dev/null +++ b/src/ipc.ts @@ -0,0 +1,53 @@ +import * as cp from "child_process"; + +import { Emitter } from "vs/base/common/event"; + +enum ControlMessage { + okToChild = "ok>", + okFromChild = "ok<", +} + +export type Message = "relaunch"; + +class IpcMain { + protected readonly _onMessage = new Emitter(); + public readonly onMessage = this._onMessage.event; + + public handshake(child?: cp.ChildProcess): Promise { + return new Promise((resolve, reject) => { + const target = child || process; + if (!target.send) { + throw new Error("Not spawned with IPC enabled"); + } + target.on("message", (message) => { + if (message === child ? ControlMessage.okFromChild : ControlMessage.okToChild) { + target.removeAllListeners(); + target.on("message", (msg) => this._onMessage.fire(msg)); + if (child) { + target.send!(ControlMessage.okToChild); + } + resolve(); + } + }); + if (child) { + child.once("error", reject); + child.once("exit", (code) => { + const error = new Error(`Unexpected exit with code ${code}`); + (error as any).code = code; + reject(error); + }); + } else { + target.send(ControlMessage.okFromChild); + } + }); + } + + public relaunch(): void { + if (!process.send) { + throw new Error("Not a child process with IPC enabled"); + } + process.send("relaunch"); + } +} + +export const ipcMain = new IpcMain(); diff --git a/src/server.ts b/src/server.ts index 22f6c254..3333602c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -53,6 +53,7 @@ import { AppInsightsAppender } from "vs/platform/telemetry/node/appInsightsAppen import { resolveCommonProperties } from "vs/platform/telemetry/node/commonProperties"; import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService"; import { TelemetryChannel } from "vs/platform/telemetry/node/telemetryIpc"; +import { UpdateChannel } from "vs/platform/update/node/updateIpc"; import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api"; import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/src/connection"; @@ -60,6 +61,7 @@ import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/sr import { TelemetryClient } from "vs/server/src/insights"; import { getNlsConfiguration, getLocaleFromConfig } from "vs/server/src/nls"; import { Protocol } from "vs/server/src/protocol"; +import { UpdateService } from "vs/server/src/update"; import { AuthType, getMediaMime, getUriTransformer, localRequire, tmpdir } from "vs/server/src/util"; export enum HttpCode { @@ -482,7 +484,11 @@ export class MainServer extends Server { REMOTE_USER_DATA_URI: transformer.transformOutgoing( (this.services.get(IEnvironmentService) as EnvironmentService).webUserDataHome, ), - PRODUCT_CONFIGURATION: product, + PRODUCT_CONFIGURATION: { + ...product, + // @ts-ignore workaround for getting the VS Code version to the browser. + version: pkg.version, + }, CONNECTION_AUTH_TOKEN: "", NLS_CONFIGURATION: await getNlsConfiguration(locale, environment.userDataPath), }; @@ -560,14 +566,13 @@ export class MainServer extends Server { this.services.set(IRequestService, new SyncDescriptor(RequestService)); this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); if (!environmentService.args["disable-telemetry"]) { - const version = `${(pkg as any).codeServerVersion || "development"}-vsc${pkg.version}`; this.services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [{ appender: combinedAppender( new AppInsightsAppender("code-server", null, () => new TelemetryClient(), logService), new LogAppender(logService), ), commonProperties: resolveCommonProperties( - product.commit, version, await getMachineId(), + product.commit, pkg.codeServerVersion, await getMachineId(), environmentService.installSourcePath, "code-server", ), piiPaths: [ @@ -601,6 +606,8 @@ export class MainServer extends Server { this.ipc.registerChannel("gallery", galleryChannel); const telemetryChannel = new TelemetryChannel(telemetryService); this.ipc.registerChannel("telemetry", telemetryChannel); + const updateChannel = new UpdateChannel(instantiationService.createInstance(UpdateService)); + this.ipc.registerChannel("update", updateChannel); resolve(new ErrorTelemetry(telemetryService)); }); }); diff --git a/src/update.ts b/src/update.ts new file mode 100644 index 00000000..0a62ddb9 --- /dev/null +++ b/src/update.ts @@ -0,0 +1,136 @@ +import * as cp from "child_process"; +import * as os from "os"; +import * as path from "path"; +import * as util from "util"; +import * as zlib from 'zlib'; + +import { CancellationToken } from "vs/base/common/cancellation"; +import * as pfs from "vs/base/node/pfs"; +import { asJson, download } from "vs/base/node/request"; +import { IConfigurationService } from "vs/platform/configuration/common/configuration"; +import { IEnvironmentService } from "vs/platform/environment/common/environment"; +import { ILogService } from "vs/platform/log/common/log"; +import pkg from "vs/platform/product/node/package"; +import { IRequestService } from "vs/platform/request/node/request"; +import { State, UpdateType, StateType, AvailableForDownload } from "vs/platform/update/common/update"; +import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService"; + +import { ipcMain } from "vs/server/src/ipc"; +import { tmpdir } from "vs/server/src/util"; +import { extract } from "vs/server/src/tar"; + +interface IUpdate { + name: string; +} + +export class UpdateService extends AbstractUpdateService { + _serviceBrand: any; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IEnvironmentService environmentService: IEnvironmentService, + @IRequestService requestService: IRequestService, + @ILogService logService: ILogService + ) { + super(null, configurationService, environmentService, requestService, logService); + } + + public async isLatestVersion(): Promise { + const latest = await this.getLatestVersion(); + return !latest || latest.name === pkg.codeServerVersion; + } + + protected buildUpdateFeedUrl(): string { + return "https://api.github.com/repos/cdr/code-server/releases/latest"; + } + + protected doQuitAndInstall(): void { + ipcMain.relaunch(); + } + + protected async doCheckForUpdates(context: any): Promise { + if (this.state.type !== StateType.Idle) { + return Promise.resolve(); + } + this.setState(State.CheckingForUpdates(context)); + try { + const update = await this.getLatestVersion(); + if (!update || !update.name || update.name === pkg.codeServerVersion) { + this.setState(State.Idle(UpdateType.Archive)); + } else { + this.setState(State.AvailableForDownload({ + version: update.name, + productVersion: update.name, + })); + } + } catch (error) { + this.onRequestError(error, !!context); + } + } + + private async getLatestVersion(): Promise { + const data = await this.requestService.request({ + url: this.url, + headers: { + "User-Agent": "code-server", + }, + }, CancellationToken.None); + return asJson(data); + } + + protected async doDownloadUpdate(state: AvailableForDownload): Promise { + this.setState(State.Updating(state.update)); + const target = os.platform(); + const releaseName = await this.buildReleaseName(state.update.version); + const url = "https://github.com/cdr/code-server/releases/download/" + + `${state.update.version}/${releaseName}` + + `.${target === "darwin" ? "zip" : "tar.gz"}`; + const downloadPath = path.join(tmpdir, `${state.update.version}-archive`); + const extractPath = path.join(tmpdir, state.update.version); + try { + await pfs.mkdirp(tmpdir); + const context = await this.requestService.request({ url }, CancellationToken.None); + // Decompress the gzip as we download. If the gzip encoding is set then + // the request service already does this. + if (target !== "darwin" && context.res.headers["content-encoding"] !== "gzip") { + context.stream = context.stream.pipe(zlib.createGunzip()); + } + await download(downloadPath, context); + await extract(downloadPath, extractPath, undefined, CancellationToken.None); + const newBinary = path.join(extractPath, releaseName, "code-server"); + if (!pfs.exists(newBinary)) { + throw new Error("No code-server binary in extracted archive"); + } + await pfs.unlink(process.argv[0]); // Must unlink first to avoid ETXTBSY. + await pfs.move(newBinary, process.argv[0]); + this.setState(State.Ready(state.update)); + } catch (error) { + this.onRequestError(error, true); + } + await Promise.all([downloadPath, extractPath].map((p) => pfs.rimraf(p))); + } + + private onRequestError(error: Error, showNotification?: boolean): void { + this.logService.error(error); + const message: string | undefined = showNotification ? (error.message || error.toString()) : undefined; + this.setState(State.Idle(UpdateType.Archive, message)); + } + + private async buildReleaseName(release: string): Promise { + let target: string = os.platform(); + if (target === "linux") { + const result = await util.promisify(cp.exec)("ldd --version"); + if (result.stderr) { + throw new Error(result.stderr); + } + if (result.stdout.indexOf("musl") !== -1) { + target = "alpine"; + } + } + let arch = os.arch(); + if (arch === "x64") { + arch = "x86_64"; + } + return `code-server${release}-${target}-${arch}`; + } +}