diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 1a027000..f9c50efa 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -124,7 +124,8 @@ import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationS import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js'; import { IMetricsService } from '../../platform/void/common/metricsService.js'; import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js'; - +import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js'; +import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js'; /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -1107,6 +1108,7 @@ export class CodeApplication extends Disposable { // Void main process services (required for services with a channel for comm between browser and electron-main (node)) services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false)); + services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false)); // Default Extensions Profile Init services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); @@ -1245,6 +1247,10 @@ export class CodeApplication extends Disposable { // Void - use loggerChannel as reference const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables); mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel); + + const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables); + mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel); + const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 2d3134d8..92707e34 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from '../../../base/common/async.js'; +// import { timeout } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; +// import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; @@ -60,7 +61,7 @@ export abstract class AbstractUpdateService implements IUpdateService { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, - @IProductService protected readonly productService: IProductService + @IProductService protected readonly productService: IProductService, ) { lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen) .finally(() => this.initialize()); @@ -79,72 +80,29 @@ export abstract class AbstractUpdateService implements IUpdateService { } console.log('is built, continuing with update service') - // Void commented this - // if (this.environmentMainService.disableUpdates) { - // this.setState(State.Disabled(DisablementReason.DisabledByEnvironment)); - // this.logService.info('update#ctor - updates are disabled by the environment'); - // return; - // } - - // if (!this.productService.updateUrl || !this.productService.commit) { - // this.setState(State.Disabled(DisablementReason.MissingConfiguration)); - // this.logService.info('update#ctor - updates are disabled as there is no update URL'); - // return; - // } - - // Void - for now, always update - - const updateMode = 'default' //this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode'); - - const quality = this.getProductQuality(updateMode); - if (!quality) { - this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); - this.logService.info('update#ctor - updates are disabled by user preference'); - return; - } - - // const quality = 'stable' - this.url = this.doBuildUpdateFeedUrl(quality); + this.url = this.doBuildUpdateFeedUrl('stable'); if (!this.url) { this.setState(State.Disabled(DisablementReason.InvalidConfiguration)); this.logService.info('update#ctor - updates are disabled as the update URL is badly formed'); return; } - // hidden setting - if (this.configurationService.getValue('_update.prss')) { - const url = new URL(this.url); - url.searchParams.set('prss', 'true'); - this.url = url.toString(); - } + this.setState(State.Disabled(DisablementReason.ManuallyDisabled)); - this.setState(State.Idle(this.getUpdateType())); - // if (updateMode === 'manual') { - // this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference'); - // return; - // } + // Void - temporarily disabled while we figure out how to do this the right way - // if (updateMode === 'start') { - // this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference'); + // this.setState(State.Idle(this.getUpdateType())); - // // Check for updates only once after 30 seconds - // setTimeout(() => this.checkForUpdates(false), 30 * 1000); - // } else { - // Start checking for updates after 30 seconds - this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err)); - // } + // start checking for updates after 10 seconds + // this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err)); } - private getProductQuality(updateMode: string): string | undefined { - return updateMode === 'none' ? undefined : this.productService.quality; - } - - private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { - await timeout(delay); - await this.checkForUpdates(false); - return await this.scheduleCheckForUpdates(60 * 60 * 1000); - } + // private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { + // await timeout(delay); + // await this.checkForUpdates(false); + // return await this.scheduleCheckForUpdates(60 * 60 * 1000); + // } async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index c521b76f..5b00195a 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @IEnvironmentMainService environmentMainService: IEnvironmentMainService, @IRequestService requestService: IRequestService, @ILogService logService: ILogService, - @IProductService productService: IProductService + @IProductService productService: IProductService, ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index c987ecce..0e2396a6 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun @ILogService logService: ILogService, @IFileService private readonly fileService: IFileService, @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IProductService productService: IProductService + @IProductService productService: IProductService, ) { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); diff --git a/src/vs/platform/void/browser/void.contribution.ts b/src/vs/platform/void/browser/void.contribution.ts index 1b3cddd2..276d6e72 100644 --- a/src/vs/platform/void/browser/void.contribution.ts +++ b/src/vs/platform/void/browser/void.contribution.ts @@ -16,3 +16,6 @@ import '../common/refreshModelService.js' // metrics import '../common/metricsService.js' + +// updates +import '../common/voidUpdateService.js' diff --git a/src/vs/platform/void/common/voidUpdateService.ts b/src/vs/platform/void/common/voidUpdateService.ts new file mode 100644 index 00000000..0304073f --- /dev/null +++ b/src/vs/platform/void/common/voidUpdateService.ts @@ -0,0 +1,46 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; + + + +export interface IVoidUpdateService { + readonly _serviceBrand: undefined; + check: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>; +} + + +export const IVoidUpdateService = createDecorator('VoidUpdateService'); + + +// implemented by calling channel +export class VoidUpdateService implements IVoidUpdateService { + + readonly _serviceBrand: undefined; + private readonly voidUpdateService: IVoidUpdateService; + + constructor( + @IMainProcessService mainProcessService: IMainProcessService, // (only usable on client side) + ) { + // creates an IPC proxy to use metricsMainService.ts + this.voidUpdateService = ProxyChannel.toService(mainProcessService.getChannel('void-channel-update')); + } + + + + // anything transmitted over a channel must be async even if it looks like it doesn't have to be + check: IVoidUpdateService['check'] = async () => { + const res = await this.voidUpdateService.check() + return res + } +} + +registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager); + + diff --git a/src/vs/platform/void/electron-main/voidUpdateMainService.ts b/src/vs/platform/void/electron-main/voidUpdateMainService.ts new file mode 100644 index 00000000..029db5f4 --- /dev/null +++ b/src/vs/platform/void/electron-main/voidUpdateMainService.ts @@ -0,0 +1,50 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; + +import { IProductService } from '../../product/common/productService.js'; + +import { IVoidUpdateService } from '../common/voidUpdateService.js'; + + + +export class VoidMainUpdateService extends Disposable implements IVoidUpdateService { + _serviceBrand: undefined; + + constructor( + @IProductService private readonly _productService: IProductService, + @IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService, + ) { + super() + } + + async check() { + const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts + + if (isDevMode) { + return { hasUpdate: false } as const + } + + try { + const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`) + const resJSON = await res.json() + + if (!resJSON) return null + + const { hasUpdate, downloadMessage } = resJSON ?? {} + if (hasUpdate === undefined) + return null + + const after = (downloadMessage || '') + '' + return { hasUpdate: !!hasUpdate, message: after } + } + catch (e) { + return null + } + } +} + diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index 8bde2321..6de0b53b 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -80,7 +80,7 @@ export class ConsistentItemService extends Disposable { } const initializeEditor = (editor: ICodeEditor) => { - if (editor.getModel()?.uri.scheme !== 'file') return + // if (editor.getModel()?.uri.scheme !== 'file') return // THIS BREAKS THINGS addTabSwitchListeners(editor) addDisposeListener(editor) putItemsOnEditor(editor, editor.getModel()?.uri ?? null) diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 1ab6ebd6..ebb17358 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -26,3 +26,6 @@ import './voidSettingsPane.js' // register css import './media/void.css' + +// update (frontend part, also see platform/) +import './voidUpdateActions.js' diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts new file mode 100644 index 00000000..e58d26ad --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts @@ -0,0 +1,87 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import Severity from '../../../../base/common/severity.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; +import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; + + + + +const notifyYesUpdate = (notifService: INotificationService, msg?: string) => { + const message = msg || 'This is a very old version of void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!' + notifService.notify({ + severity: Severity.Info, + message: message, + }) +} +const notifyNoUpdate = (notifService: INotificationService) => { + notifService.notify({ + severity: Severity.Info, + message: 'Void is up-to-date!', + }) +} +const notifyErrChecking = (notifService: INotificationService) => { + const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!` + notifService.notify({ + severity: Severity.Info, + message: message, + }) +} + + + +// Action +registerAction2(class extends Action2 { + constructor() { + super({ + f1: true, + id: 'void.voidCheckUpdate', + title: localize2('voidCheckUpdate', 'Void: Check for Updates'), + }); + } + async run(accessor: ServicesAccessor): Promise { + const voidUpdateService = accessor.get(IVoidUpdateService) + const notifService = accessor.get(INotificationService) + const metricsService = accessor.get(IMetricsService) + + const res = await voidUpdateService.check() + if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update: Error', {}) } + else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update: Yes', {}) } + else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update: No', {}) } + } +}) + +// on mount +class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.void.voidUpdate' + constructor( + @IVoidUpdateService private readonly voidUpdateService: IVoidUpdateService, + @INotificationService private readonly notifService: INotificationService, + @IMetricsService private readonly metricsService: IMetricsService, + ) { + super() + + // on mount + setTimeout(async () => { + const res = await this.voidUpdateService.check() + + const notifService = this.notifService + const metricsService = this.metricsService + + if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Startup: Error', {}) } + else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); metricsService.capture('Void Update Startup: Yes', {}) } + else if (!res.hasUpdate) { metricsService.capture('Void Update Startup: No', {}) } // display nothing if up to date + + }, 5 * 1000) + } +} +registerWorkbenchContribution2(VoidUpdateWorkbenchContribution.ID, VoidUpdateWorkbenchContribution, WorkbenchPhase.BlockRestore);