Skip to content

Commit

Permalink
ovsx-client: introduce async ovsx-client provider (#10327)
Browse files Browse the repository at this point in the history
The commit fixes an issue where the api version was not read properly or on time in the backend leading to installing a potentially incompatible version of an extension (as it would not respect the declared api). The code was refactored in a way to wait until the api version was ready from either the environment variable or cli contribution.

The changes will now create the ovsclient only when the proper values are ready fixing any potential problems caused by previous timing issues.

The changes should:
- use the default api if required for both the frontend and backend
- use the cli contribution (provided by `--vscode-api-version`) overriding the default for both the frontend and backend
- use the environment variable overriding the default for both the frontend and backend

The changes will now properly respect the api version when searching extension, and installing them (resolving them in the backend).

Any potential breaking changes are documented in the `changelog`.

Signed-off-by: vince-fugnitto <[email protected]>
Co-authored-by: Paul Marechal <[email protected]>
  • Loading branch information
vince-fugnitto and paul-marechal authored Oct 28, 2021
1 parent 5049437 commit bf00fd0
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 113 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
- [core] moved `NewWindowOptions` to `common/window.ts` [#10291](https://github.com/eclipse-theia/theia/pull/10291)
- [ovsx-client] removed `fetchJson` method from `OVSXClient` [#10325](https://github.com/eclipse-theia/theia/pull/10325)
- [callhierarchy, plugin-ext] Call hierarchy methods `getRootDefinition`, `$provideRootDefinition`, `provideRootDefinition`, and `prepareCallHierarchy` retyped to allow a return of an item or an array of items. [#10310](https://github.com/eclipse-theia/theia/pull/10310)
- [vsx-registry] removed `OVSXAsyncClient` [#10327](https://github.com/eclipse-theia/theia/pull/10327)
- [vsx-registry] updated `VSXEnvironment` from a class to an interface and symbol implemented in both `browser` and `node` [#10327](https://github.com/eclipse-theia/theia/pull/10327)

## v1.18.0 - 9/30/2021

Expand Down
1 change: 1 addition & 0 deletions dev-packages/ovsx-client/src/ovsx-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface OVSXClientOptions {
apiVersion: string
apiUrl: string
}

export class OVSXClient {

constructor(readonly options: OVSXClientOptions) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@ import { Argv, Arguments } from '@theia/core/shared/yargs';
import { CliContribution } from '@theia/core/lib/node/cli';
import { PluginHostEnvironmentVariable } from '@theia/plugin-ext/lib/common';
import { VSCODE_DEFAULT_API_VERSION } from '../common/plugin-vscode-types';
import { Deferred } from '@theia/core/lib/common/promise-util';

/**
* CLI Contribution allowing to override the VS Code API version which is returned by `vscode.version` API call.
*/
@injectable()
export class PluginVsCodeCliContribution implements CliContribution, PluginHostEnvironmentVariable {

/**
* CLI argument name to define the supported VS Code API version.
*/
static VSCODE_API_VERSION = 'vscode-api-version';

protected vsCodeApiVersion: string | undefined;
protected vsCodeApiVersion?: string;
protected vsCodeApiVersionDeferred = new Deferred<string>();

get vsCodeApiVersionPromise(): Promise<string> {
return this.vsCodeApiVersionDeferred.promise;
}

configure(conf: Argv): void {
conf.option(PluginVsCodeCliContribution.VSCODE_API_VERSION, {
Expand All @@ -40,11 +49,10 @@ export class PluginVsCodeCliContribution implements CliContribution, PluginHostE
}

setArguments(args: Arguments): void {
const arg = args[PluginVsCodeCliContribution.VSCODE_API_VERSION] as string;
if (arg) {
this.vsCodeApiVersion = arg;
this.process(process.env);
}
const arg = args[PluginVsCodeCliContribution.VSCODE_API_VERSION] as string | undefined;
this.vsCodeApiVersion = arg?.trim() || process.env['VSCODE_API_VERSION']?.trim() || VSCODE_DEFAULT_API_VERSION;
process.env['VSCODE_API_VERSION'] = this.vsCodeApiVersion;
this.vsCodeApiVersionDeferred.resolve(this.vsCodeApiVersion);
}

process(env: NodeJS.ProcessEnv): void {
Expand Down
2 changes: 1 addition & 1 deletion packages/vsx-registry/src/browser/vsx-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export class VSXExtension implements VSXExtensionData, TreeElement {
* @returns the registry link for the given extension at the path.
*/
async getRegistryLink(path = ''): Promise<URI> {
const uri = await this.environment.getRegistryUri();
const uri = new URI(await this.environment.getRegistryUri());
return uri.resolve('extension/' + this.id.replace('.', '/')).resolve(path);
}

Expand Down
18 changes: 10 additions & 8 deletions packages/vsx-registry/src/browser/vsx-extensions-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ import { WorkspaceService } from '@theia/workspace/lib/browser';
import { RecommendedExtensions } from './recommended-extensions/recommended-extensions-preference-contribution';
import URI from '@theia/core/lib/common/uri';
import { VSXResponseError, VSXSearchParam } from '@theia/ovsx-client/lib/ovsx-types';
import { OVSXAsyncClient } from './ovsx-async-client';
import { OVSXClientProvider } from '../common/ovsx-client-provider';

@injectable()
export class VSXExtensionsModel {

protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.onDidChangeEmitter.event;

@inject(OVSXAsyncClient)
protected client: OVSXAsyncClient;
@inject(OVSXClientProvider)
protected clientProvider: OVSXClientProvider;

@inject(HostedPluginSupport)
protected readonly pluginSupport: HostedPluginSupport;
Expand All @@ -63,7 +63,6 @@ export class VSXExtensionsModel {

@postConstruct()
protected async init(): Promise<void> {
await this.client.ready;
await Promise.all([
this.initInstalled(),
this.initSearchResult(),
Expand Down Expand Up @@ -167,14 +166,15 @@ export class VSXExtensionsModel {
}, 150);
protected doUpdateSearchResult(param: VSXSearchParam, token: CancellationToken): Promise<void> {
return this.doChange(async () => {
const result = await this.client.search(param);
const client = await this.clientProvider();
const result = await client.search(param);
if (token.isCancellationRequested) {
return;
}
const searchResult = new Set<string>();
for (const data of result.extensions) {
const id = data.namespace.toLowerCase() + '.' + data.name.toLowerCase();
const extension = this.client.getLatestCompatibleVersion(data);
const extension = client.getLatestCompatibleVersion(data);
if (!extension) {
continue;
}
Expand Down Expand Up @@ -260,7 +260,8 @@ export class VSXExtensionsModel {
}
if (extension.readmeUrl) {
try {
const rawReadme = await this.client.fetchText(extension.readmeUrl);
const client = await this.clientProvider();
const rawReadme = await client.fetchText(extension.readmeUrl);
const readme = this.compileReadme(rawReadme);
extension.update({ readme });
} catch (e) {
Expand Down Expand Up @@ -292,7 +293,8 @@ export class VSXExtensionsModel {
if (!this.shouldRefresh(extension)) {
return extension;
}
const data = await this.client.getLatestCompatibleExtensionVersion(id);
const client = await this.clientProvider();
const data = await client.getLatestCompatibleExtensionVersion(id);
if (!data) {
return;
}
Expand Down
24 changes: 11 additions & 13 deletions packages/vsx-registry/src/browser/vsx-registry-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import '../../src/browser/style/index.css';

import { ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory, bindViewContribution, FrontendApplicationContribution, ViewContainerIdentifier, OpenHandler, WidgetManager } from '@theia/core/lib/browser';
import {
WidgetFactory, bindViewContribution, FrontendApplicationContribution, ViewContainerIdentifier, OpenHandler, WidgetManager, WebSocketConnectionProvider
} from '@theia/core/lib/browser';
import { VSXExtensionsViewContainer } from './vsx-extensions-view-container';
import { VSXExtensionsContribution } from './vsx-extensions-contribution';
import { VSXExtensionsSearchBar } from './vsx-extensions-search-bar';
Expand All @@ -28,24 +30,20 @@ import { VSXExtensionFactory, VSXExtension, VSXExtensionOptions } from './vsx-ex
import { VSXExtensionEditor } from './vsx-extension-editor';
import { VSXExtensionEditorManager } from './vsx-extension-editor-manager';
import { VSXExtensionsSourceOptions } from './vsx-extensions-source';
import { VSXEnvironment } from '../common/vsx-environment';
import { VSXExtensionsSearchModel } from './vsx-extensions-search-model';
import { bindExtensionPreferences } from './recommended-extensions/recommended-extensions-preference-contribution';
import { bindPreferenceProviderOverrides } from './recommended-extensions/preference-provider-overrides';
import { OVSXAsyncClient } from './ovsx-async-client';
import { OVSXClientProvider, createOVSXClient } from '../common/ovsx-client-provider';
import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment';

export default new ContainerModule((bind, unbind) => {
bind(VSXEnvironment).toSelf().inSingletonScope();
bind(OVSXAsyncClient).toDynamicValue(ctx => {
const vsxEnvironment = ctx.container.get(VSXEnvironment);
return new OVSXAsyncClient(Promise.all([
vsxEnvironment.getVscodeApiVersion(),
vsxEnvironment.getRegistryApiUri()
]).then(([apiVersion, apiUri]) => ({
apiVersion,
apiUrl: apiUri.toString()
})));
bind<OVSXClientProvider>(OVSXClientProvider).toDynamicValue(ctx => {
const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment));
return () => clientPromise;
}).inSingletonScope();
bind(VSXEnvironment).toDynamicValue(
ctx => WebSocketConnectionProvider.createProxy(ctx.container, VSX_ENVIRONMENT_PATH)
).inSingletonScope();

bind(VSXExtension).toSelf();
bind(VSXExtensionFactory).toFactory(ctx => (option: VSXExtensionOptions) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,16 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { OVSXClient, OVSXClientOptions } from '@theia/ovsx-client/lib';
import { OVSXClient } from '@theia/ovsx-client';
import { VSXEnvironment } from './vsx-environment';

/**
* In some instances, the OVSXClient must be created asynchronously. This class
* makes it possible to get an un-initialized instance and wait for it to be ready.
*/
export class OVSXAsyncClient extends OVSXClient {
export const OVSXClientProvider = Symbol('OVSXClientProvider');
export type OVSXClientProvider = () => Promise<OVSXClient>;

/**
* Resolves once the initial asynchronous options are resolved.
*
* Calling methods before this promise is resolved will throw errors.
*/
readonly ready: Promise<OVSXAsyncClient>;

constructor(asyncOptions: Promise<OVSXClientOptions>) {
super(undefined!); // hack: using methods at this point will fail.
this.ready = asyncOptions.then(options => {
(this.options as OVSXClientOptions) = options;
return this;
});
}
export async function createOVSXClient(vsxEnvironment: VSXEnvironment): Promise<OVSXClient> {
const [apiVersion, apiUrl] = await Promise.all([
vsxEnvironment.getVscodeApiVersion(),
vsxEnvironment.getRegistryApiUri()
]);
return new OVSXClient({ apiVersion, apiUrl: apiUrl.toString() });
}
24 changes: 24 additions & 0 deletions packages/vsx-registry/src/common/vsx-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

export const VSX_ENVIRONMENT_PATH = '/services/vsx-environment';

export const VSXEnvironment = Symbol('VSXEnvironment');
export interface VSXEnvironment {
getRegistryUri(): Promise<string>;
getRegistryApiUri(): Promise<string>;
getVscodeApiVersion(): Promise<string>;
}
51 changes: 0 additions & 51 deletions packages/vsx-registry/src/common/vsx-environment.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions packages/vsx-registry/src/node/vsx-environment-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/********************************************************************************
* Copyright (C) 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { PluginVsCodeCliContribution } from '@theia/plugin-ext-vscode/lib/node/plugin-vscode-cli-contribution';
import { VSXEnvironment } from '../common/vsx-environment';

@injectable()
export class VSXEnvironmentImpl implements VSXEnvironment {

protected _registryUri = new URI(process.env['VSX_REGISTRY_URL']?.trim() || 'https://open-vsx.org');

@inject(PluginVsCodeCliContribution)
protected readonly pluginVscodeCli: PluginVsCodeCliContribution;

async getRegistryUri(): Promise<string> {
return this._registryUri.toString(true);
}

async getRegistryApiUri(): Promise<string> {
return this._registryUri.resolve('api').toString(true);
}

async getVscodeApiVersion(): Promise<string> {
return this.pluginVscodeCli.vsCodeApiVersionPromise;
}
}
9 changes: 5 additions & 4 deletions packages/vsx-registry/src/node/vsx-extension-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { PluginDeployerResolver, PluginDeployerResolverContext } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { VSXExtensionUri } from '../common/vsx-extension-uri';
import { OVSXClient } from '@theia/ovsx-client/lib/ovsx-client';
import { OVSXClientProvider } from '../common/ovsx-client-provider';

@injectable()
export class VSXExtensionResolver implements PluginDeployerResolver {

@inject(OVSXClient)
protected client: OVSXClient;
@inject(OVSXClientProvider)
protected clientProvider: OVSXClientProvider;

protected readonly downloadPath: string;

Expand All @@ -49,7 +49,8 @@ export class VSXExtensionResolver implements PluginDeployerResolver {
return;
}
console.log(`[${id}]: trying to resolve latest version...`);
const extension = await this.client.getLatestCompatibleExtensionVersion(id);
const client = await this.clientProvider();
const extension = await client.getLatestCompatibleExtensionVersion(id);
if (!extension) {
return;
}
Expand Down
22 changes: 12 additions & 10 deletions packages/vsx-registry/src/node/vsx-registry-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { VSXExtensionResolver } from './vsx-extension-resolver';
import { PluginDeployerResolver } from '@theia/plugin-ext/lib/common/plugin-protocol';
import { VSCODE_DEFAULT_API_VERSION, VSX_REGISTRY_URL_DEFAULT } from '@theia/plugin-ext-vscode/lib/common/plugin-vscode-types';
import { OVSXClient } from '@theia/ovsx-client/lib/ovsx-client';
import { OVSXClientProvider, createOVSXClient } from '../common/ovsx-client-provider';
import { VSXEnvironment, VSX_ENVIRONMENT_PATH } from '../common/vsx-environment';
import { VSXEnvironmentImpl } from './vsx-environment-impl';
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';

export default new ContainerModule(bind => {
bind(OVSXClient).toConstantValue(new OVSXClient({
apiVersion: process.env['VSCODE_API_VERSION'] || VSCODE_DEFAULT_API_VERSION,
apiUrl: resolveRegistryUrl()
}));
bind<OVSXClientProvider>(OVSXClientProvider).toDynamicValue(ctx => {
const clientPromise = createOVSXClient(ctx.container.get(VSXEnvironment));
return () => clientPromise;
}).inSingletonScope();
bind(VSXEnvironment).to(VSXEnvironmentImpl).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(
ctx => new JsonRpcConnectionHandler(VSX_ENVIRONMENT_PATH, () => ctx.container.get(VSXEnvironment))
).inSingletonScope();
bind(VSXExtensionResolver).toSelf().inSingletonScope();
bind(PluginDeployerResolver).toService(VSXExtensionResolver);
});

function resolveRegistryUrl(): string {
return process.env['VSX_REGISTRY_URL'] || VSX_REGISTRY_URL_DEFAULT;
}

0 comments on commit bf00fd0

Please sign in to comment.