From 0564786b6cf52ced785bdba98287051669759864 Mon Sep 17 00:00:00 2001 From: Tobias Ortmayr Date: Tue, 14 Feb 2023 17:39:53 +0100 Subject: [PATCH] Refactor and cleanup GLSPContribution API - Refactor `GLSPCLientProvider` and reuse it in `BaseGLSPTheiaConnector` - Make GLSPContributions disposable - Cleanup redundant session concept in `GLSPBackendContribution` - Cleanup & refactor`GLSPClientContribution` API. - Cleanup & refactor`GLSPSocketServerContribution` API - Remove remains from @theia/languages - Simplify client initialization by using Theia's Deferred concept. Fixes https://github.com/eclipse-glsp/glsp/issues/848 --- .../diagram/base-theia-glsp-connector.ts | 38 ++-- .../src/browser/glsp-client-contribution.ts | 167 ++++++++---------- .../src/browser/glsp-client-provider.ts | 45 +++-- .../src/browser/glsp-frontend-contribution.ts | 11 +- .../theia-integration-frontend-module.ts | 7 +- .../src/common/glsp-contribution.ts | 17 +- .../src/node/glsp-backend-contribution.ts | 41 ++--- .../src/node/glsp-server-contribution.ts | 46 ++--- .../node/glsp-socket-server-contribution.ts | 74 ++++---- 9 files changed, 209 insertions(+), 237 deletions(-) diff --git a/packages/theia-integration/src/browser/diagram/base-theia-glsp-connector.ts b/packages/theia-integration/src/browser/diagram/base-theia-glsp-connector.ts index e0bd9b4..6ea21ab 100644 --- a/packages/theia-integration/src/browser/diagram/base-theia-glsp-connector.ts +++ b/packages/theia-integration/src/browser/diagram/base-theia-glsp-connector.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019-2022 EclipseSource and others. + * Copyright (c) 2019-2023 EclipseSource 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 @@ -23,15 +23,16 @@ import { ServerMessageAction, ServerStatusAction } from '@eclipse-glsp/client'; -import { ContributionProvider, Message, MessageService, MessageType } from '@theia/core'; +import { Message, MessageService, MessageType } from '@theia/core'; import { ConfirmDialog, WidgetManager } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; -import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { EditorManager } from '@theia/editor/lib/browser'; import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/file-dialog-service'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { DiagramWidget, TheiaDiagramServer } from 'sprotty-theia'; import { GLSPClientContribution } from '../glsp-client-contribution'; +import { GLSPClientProvider } from '../glsp-client-provider'; import { deriveDiagramManagerId } from './glsp-diagram-manager'; import { GLSPMessageOptions, GLSPNotificationManager } from './glsp-notification-manager'; import { TheiaGLSPConnector } from './theia-glsp-connector'; @@ -58,9 +59,8 @@ export abstract class BaseTheiaGLSPConnector implements TheiaGLSPConnector { @inject(GLSPNotificationManager) protected readonly notificationManager: GLSPNotificationManager; - @inject(ContributionProvider) - @named(GLSPClientContribution) - protected readonly clientContributions: ContributionProvider; + @inject(GLSPClientProvider) + protected readonly glspClientProvider: GLSPClientProvider; private servers: Map = new Map(); private widgetMessages: Map = new Map(); @@ -68,27 +68,29 @@ export abstract class BaseTheiaGLSPConnector implements TheiaGLSPConnector { abstract readonly diagramType: string; abstract readonly contributionId: string; + protected glspClientContribution: GLSPClientContribution; @postConstruct() protected initialize(): void { - const contributions = this.clientContributions.getContributions().filter(contribution => contribution.id === this.contributionId); - if (contributions.length === 0) { - throw new Error(`Could not retrieve GLSP client contribution with id '${this.contributionId}}'`); + const clientContribution = this.glspClientProvider.getGLSPClientContribution(this.contributionId); + if (!clientContribution) { + throw new Error(`No GLSPClientContribution is configured for the id '${this.contributionId}'`); } - this.glspClientContribution = contributions[0]; - this.glspClientContribution.glspClient.then(client => client.onActionMessage(this.onMessageReceived.bind(this))); + this.glspClientContribution = clientContribution; } connect(diagramServer: TheiaDiagramServer): void { this.servers.set(diagramServer.clientId, diagramServer); - this.glspClient.then(client => + + this.glspClient.then(client => { + client.onActionMessage(message => this.onMessageReceived(message)); client.initializeClientSession({ clientSessionId: diagramServer.clientId, diagramType: this.diagramType, args: this.initializeClientSessionArgs(diagramServer) - }) - ); + }); + }); diagramServer.connect(this); } @@ -113,15 +115,15 @@ export abstract class BaseTheiaGLSPConnector implements TheiaGLSPConnector { async save(uri: string, action: ExportSvgAction): Promise { const folder = await this.fileService.resolve(new URI(uri)); - let file = await this.fileDialogService.showSaveDialog({ title: 'Export Diagram' , filters: { 'Images (*.svg)': ['svg'] }}, folder); - if(file) { + let file = await this.fileDialogService.showSaveDialog({ title: 'Export Diagram', filters: { 'Images (*.svg)': ['svg'] } }, folder); + if (file) { try { - if(!file.path.ext) { + if (!file.path.ext) { file = new URI(file.path.fsPath() + '.svg'); } await this.fileService.write(file, action.svg); this.messageService.info(`Diagram exported to '${file.path.name}'`); - } catch(error) { + } catch (error) { this.messageService.info(`Error exporting diagram '${error}'`); } } diff --git a/packages/theia-integration/src/browser/glsp-client-contribution.ts b/packages/theia-integration/src/browser/glsp-client-contribution.ts index cd016d9..cc73e65 100644 --- a/packages/theia-integration/src/browser/glsp-client-contribution.ts +++ b/packages/theia-integration/src/browser/glsp-client-contribution.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2017-2021 TypeFox and others. + * Copyright (C) 2019-2023 EclipseSource 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 @@ -16,7 +16,6 @@ import { ApplicationIdProvider, Args, - ClientState, ConnectionProvider, GLSPClient, InitializeParameters, @@ -25,9 +24,8 @@ import { } from '@eclipse-glsp/client'; import { Disposable, DisposableCollection, MessageService } from '@theia/core'; import { FrontendApplication, WebSocketConnectionProvider } from '@theia/core/lib/browser'; -import { inject, injectable, multiInject } from '@theia/core/shared/inversify'; -import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { DiagramManagerProvider } from 'sprotty-theia'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { inject, injectable } from '@theia/core/shared/inversify'; import 'sprotty-theia/css/theia-sprotty.css'; import 'sprotty/css/sprotty.css'; import '../../css/command-palette.css'; @@ -41,37 +39,65 @@ import { TheiaJsonrpcGLSPClient } from './theia-jsonrpc-glsp-client'; export const GLSPClientContribution = Symbol.for('GLSPClientContribution'); +/** + * The frontend service component of a {@link GLSPContribution}. Responsible for providing & initializing the + * {@link GLSPClient}. + */ export interface GLSPClientContribution extends GLSPContribution { - readonly running: boolean; - readonly initializeResult: Promise; - readonly glspClient: Promise; - waitForActivation(app: FrontendApplication): Promise; - activate(app: FrontendApplication): Disposable; + /** + * Triggers the setup for the {@link GLSPClient}. + * The activation phase consists of the following steps: + * - Establish a service connection to the corresponding backend contribution (`GLSPServerContribution`) + * - Create a new {@link GLSPClient} on top of the service connection + * - Start the client + * - Initialize the server + * + * The {@link GLSPClientContribution.waitForActivation} function can be used to further delay the activation + * @param app Theia`s frontend application + */ + activate(app: FrontendApplication): void; + + /** + * Optional function to delay the activation of this client contribution until certain conditions are met + * @param app Theia`s frontend application + * @returns A promise that resolves once all activation conditions are met. + */ + waitForActivation?(app: FrontendApplication): Promise; + + /** + * Deactivates the contribution and disposes all underlying resources e.g. the service connection + * and the glsp client. + * + * @param app Theia`s frontend application + */ deactivate(app: FrontendApplication): void; + + /** + * Retrieve the activated {@link GLSPClient}. + * @returns A promise of the client that resolves after the client has been started & initialized + */ + readonly glspClient: Promise; + + /** + * The cached result of the client initialization + * @returns A promise that will resolve after {@link GLSPClient.initializeServer} has been called + */ + readonly initializeResult: Promise; } @injectable() export abstract class BaseGLSPClientContribution implements GLSPClientContribution { abstract readonly id: string; - protected _glspClient: GLSPClient | undefined; - - protected resolveReady: (glspClient: GLSPClient) => void; - protected ready: Promise; - protected readonly toDeactivate = new DisposableCollection(); + protected glspClientDeferred: Deferred = new Deferred(); + protected readonly toDispose = new DisposableCollection(); protected _initializeResult: InitializeResult | undefined; - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(MessageService) protected readonly messageService: MessageService; @inject(WebSocketConnectionProvider) protected readonly connectionProvider: WebSocketConnectionProvider; - @multiInject(DiagramManagerProvider) protected diagramManagerProviders: DiagramManagerProvider[]; - - constructor() { - this.waitForReady(); - } get glspClient(): Promise { - return this._glspClient ? Promise.resolve(this._glspClient) : this.ready; + return this.glspClientDeferred.promise; } get initializeResult(): Promise { @@ -83,84 +109,58 @@ export abstract class BaseGLSPClientContribution implements GLSPClientContributi }); } - waitForActivation(app: FrontendApplication): Promise { - const activationPromises: Promise[] = []; - const workspaceContains = this.workspaceContains; - if (workspaceContains.length !== 0) { - activationPromises.push(this.waitForItemInWorkspace()); - } - if (activationPromises.length !== 0) { - return Promise.all([ - this.ready, - Promise.race( - activationPromises.map( - p => - // eslint-disable-next-line no-async-promise-executor - new Promise(async resolve => { - try { - await p; - resolve(); - } catch (e) { - console.error(e); - } - }) - ) - ) - ]); - } - return this.ready; - } + waitForActivation?(app: FrontendApplication): Promise; - activate(): Disposable { - if (this.toDeactivate.disposed) { + activate(app: FrontendApplication): void { + if (this.toDispose.disposed) { // eslint-disable-next-line @typescript-eslint/no-empty-function - this.toDeactivate.push(new DisposableCollection(Disposable.create(() => {}))); // mark as not disposed - this.doActivate(this.toDeactivate); + this.toDispose.push(new DisposableCollection(Disposable.create(() => {}))); // mark as not disposed + if (this.waitForActivation) { + this.waitForActivation(app).then(() => this.doActivate()); + return; + } + this.doActivate(); } - return this.toDeactivate; } deactivate(_app: FrontendApplication): void { - this.toDeactivate.dispose(); + this.toDispose.dispose(); } - protected async doActivate(toStop: DisposableCollection): Promise { + protected async doActivate(): Promise { try { this.connectionProvider.listen( { path: GLSPContribution.getPath(this), onConnection: channel => { - if (toStop.disposed) { + if (this.toDispose.disposed) { channel.close(); return; } const connection = createChannelConnection(channel); - const languageClient = this.createGLSPCLient(connection); - this.onWillStart(languageClient); - toStop.pushAll([ + const client = this.createGLSPClient(connection); + this.start(client); + this.toDispose.pushAll([ Disposable.create(() => { channel.close(); - languageClient.shutdownServer(); - languageClient.stop(); + client.shutdownServer(); + client.stop(); }) ]); } }, - { reconnecting: false } + { reconnecting: true } ); } catch (e) { console.error(e); + this.glspClientDeferred.reject(e); } } - get running(): boolean { - return !this.toDeactivate.disposed && this._glspClient !== undefined && this._glspClient.currentState === ClientState.Running; - } - - protected async onWillStart(languageClient: GLSPClient): Promise { - await languageClient.start(); - this._initializeResult = await this.initialize(languageClient); - this.onReady(languageClient); + protected async start(glspClient: GLSPClient): Promise { + await glspClient.start(); + this._initializeResult = await this.initialize(glspClient); + this.glspClientDeferred.resolve(glspClient); } protected async initialize(languageClient: GLSPClient): Promise { @@ -187,17 +187,7 @@ export abstract class BaseGLSPClientContribution implements GLSPClientContributi return undefined; } - protected onReady(languageClient: GLSPClient): void { - this._glspClient = languageClient; - this.resolveReady(this._glspClient); - this.waitForReady(); - } - - protected waitForReady(): void { - this.ready = new Promise(resolve => (this.resolveReady = resolve)); - } - - protected createGLSPCLient(connectionProvider: ConnectionProvider): GLSPClient { + protected createGLSPClient(connectionProvider: ConnectionProvider): GLSPClient { return new TheiaJsonrpcGLSPClient({ id: this.id, connectionProvider, @@ -205,16 +195,7 @@ export abstract class BaseGLSPClientContribution implements GLSPClientContributi }); } - protected get workspaceContains(): string[] { - return []; - } - - protected async waitForItemInWorkspace(): Promise { - const doesContain = await this.workspaceService.containsSome(this.workspaceContains); - if (!doesContain) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - return new Promise(resolve => {}); - } - return doesContain; + dispose(): void { + this.toDispose.dispose(); } } diff --git a/packages/theia-integration/src/browser/glsp-client-provider.ts b/packages/theia-integration/src/browser/glsp-client-provider.ts index 775076b..f443455 100644 --- a/packages/theia-integration/src/browser/glsp-client-provider.ts +++ b/packages/theia-integration/src/browser/glsp-client-provider.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019-2021 EclipseSource and others. + * Copyright (c) 2023 EclipseSource 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 @@ -18,35 +18,30 @@ import { ContributionProvider } from '@theia/core'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { GLSPClientContribution } from './glsp-client-contribution'; -export const GLSPClientProvider = Symbol.for('GLSPClientProvider'); - -export interface GLSPClientProvider { - getLanguageClient(languageId: string): Promise; -} - +/** + * Provides lookup methods to retrieve a glsp client (or its contribution) via id + */ @injectable() -export class GLSPClientProviderImpl implements GLSPClientProvider { +export class GLSPClientProvider { @inject(ContributionProvider) @named(GLSPClientContribution) - private readonly contributions: ContributionProvider; + protected readonly contributors: ContributionProvider; - async getLanguageClient(languageId: string): Promise { - const contribution = this.getLanguageContribution(languageId); - if (contribution) { - return contribution.glspClient; - } - return undefined; + /** + * Look up the {@link GLSPClient} that is configured for the contribution Id (if any). + * @param contributionId The contributionId of the target client + * @returns the corresponding `GLSPClient` or `undefined` if no client is configured for the given type + */ + async getGLSPClient(contributionId: string): Promise { + return this.getGLSPClientContribution(contributionId)?.glspClient ?? undefined; } - protected getLanguageContribution(languageId: string): GLSPClientContribution | undefined { - const contributions = this.contributions.getContributions(); - if (contributions) { - for (const contribution of contributions) { - if (contribution.id === languageId) { - return contribution; - } - } - } - return undefined; + /** + * Look up the {@link GLSPClientContribution} that is configured for the given contribution Id (if any). + * @param contributionId The contributionId of the target contribution + * @returns the corresponding `GLSPClientContribution` or `undefined` if no client contribution is configured for the given type + */ + getGLSPClientContribution(contributionId: string): GLSPClientContribution | undefined { + return this.contributors.getContributions().find(contribution => contribution.id === contributionId); } } diff --git a/packages/theia-integration/src/browser/glsp-frontend-contribution.ts b/packages/theia-integration/src/browser/glsp-frontend-contribution.ts index 9162a91..1430de5 100644 --- a/packages/theia-integration/src/browser/glsp-frontend-contribution.ts +++ b/packages/theia-integration/src/browser/glsp-frontend-contribution.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019-2021 EclipseSource and others. + * Copyright (c) 2019-2023 EclipseSource 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 @@ -19,6 +19,9 @@ import { inject, injectable, named } from '@theia/core/shared/inversify'; import { GLSPClientContribution } from './glsp-client-contribution'; +/** + * Responsible for the activation & deactivation of all registered {@link GLSPClientContribution}s. + */ @injectable() export class GLSPFrontendContribution implements FrontendApplicationContribution { @inject(FrontendApplication) @@ -26,16 +29,16 @@ export class GLSPFrontendContribution implements FrontendApplicationContribution @inject(ContributionProvider) @named(GLSPClientContribution) - protected readonly contributions: ContributionProvider; + protected readonly contributors: ContributionProvider; onStart(app: FrontendApplication): void { - for (const contribution of this.contributions.getContributions()) { + for (const contribution of this.contributors.getContributions()) { contribution.activate(app); } } onStop(app: FrontendApplication): void { - for (const contribution of this.contributions.getContributions()) { + for (const contribution of this.contributors.getContributions()) { contribution.deactivate(app); } } diff --git a/packages/theia-integration/src/browser/theia-integration-frontend-module.ts b/packages/theia-integration/src/browser/theia-integration-frontend-module.ts index cf75292..39ebab6 100644 --- a/packages/theia-integration/src/browser/theia-integration-frontend-module.ts +++ b/packages/theia-integration/src/browser/theia-integration-frontend-module.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2019-2021 EclipseSource and others. + * Copyright (c) 2019-2023 EclipseSource 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 @@ -26,7 +26,7 @@ import { TheiaContextMenuServiceFactory } from './diagram/theia-context-menu-ser import { TheiaGLSPConnector, TheiaGLSPConnectorRegistry } from './diagram/theia-glsp-connector'; import { TheiaMarkerManager, TheiaMarkerManagerFactory } from './diagram/theia-marker-manager'; import { GLSPClientContribution } from './glsp-client-contribution'; -import { GLSPClientProvider, GLSPClientProviderImpl } from './glsp-client-provider'; +import { GLSPClientProvider } from './glsp-client-provider'; import { GLSPFrontendContribution } from './glsp-frontend-contribution'; import { TheiaSourceModelChangedHandler } from './theia-model-source-changed-handler'; import { TheiaOpenerOptionsNavigationService } from './theia-opener-options-navigation-service'; @@ -36,8 +36,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(GLSPFrontendContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(GLSPFrontendContribution); - bind(GLSPClientProviderImpl).toSelf().inSingletonScope(); - bind(GLSPClientProvider).toService(GLSPClientProviderImpl); + bind(GLSPClientProvider).toSelf().inSingletonScope(); bind(GLSPContribution.Service) .toDynamicValue(({ container }) => WebSocketConnectionProvider.createProxy(container, GLSPContribution.servicePath)) diff --git a/packages/theia-integration/src/common/glsp-contribution.ts b/packages/theia-integration/src/common/glsp-contribution.ts index b89ced6..01ce36c 100644 --- a/packages/theia-integration/src/common/glsp-contribution.ts +++ b/packages/theia-integration/src/common/glsp-contribution.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2020-2021 EclipseSource and others. + * Copyright (C) 2020-2023 EclipseSource 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 @@ -14,18 +14,19 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export interface GLSPContribution { +import { Disposable } from '@theia/core'; + +/** + * Common interface for `GLSPClientContribution` (frontend) and `GLSPServerContribution` (backend). + */ +export interface GLSPContribution extends Disposable { readonly id: string; } export namespace GLSPContribution { export const servicePath = '/services/glsp'; - export function getPath(contribution: GLSPContribution, sessionId = ':id'): string { - return servicePath + '/' + contribution.id + '/' + sessionId; + export function getPath(contribution: GLSPContribution): string { + return servicePath + '/' + contribution.id; } export const Service = Symbol('GLSPContributionService'); - export interface Service { - create(contributionId: string, parameters: any): Promise; - destroy(sessionId: string): Promise; - } } diff --git a/packages/theia-integration/src/node/glsp-backend-contribution.ts b/packages/theia-integration/src/node/glsp-backend-contribution.ts index e59adb2..c75e1ba 100644 --- a/packages/theia-integration/src/node/glsp-backend-contribution.ts +++ b/packages/theia-integration/src/node/glsp-backend-contribution.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018-2022 TypeFox and others. + * Copyright (C) 2018-2023 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 @@ -13,42 +13,29 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { ContributionProvider, ILogger } from '@theia/core/lib/common'; +import { ContributionProvider } from '@theia/core/lib/common'; import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { GLSPContribution } from '../common'; import { GLSPServerContribution, GLSPServerContributionOptions } from './glsp-server-contribution'; +/** + * Responsible for the configuration of all registered {@link GLSPServerContribution}s. + * This includes two main steps: + * - launch the GLSP server process (if necessary) + * - forwarding of the service connection to the `GLSPClientContribution` counterpart i.e. the client channel + */ @injectable() -export class GLSPBackendContribution implements MessagingService.Contribution, GLSPContribution.Service { - @inject(ILogger) - @named('glsp') - protected readonly logger: ILogger; - +export class GLSPBackendContribution implements MessagingService.Contribution { @inject(ContributionProvider) @named(GLSPServerContribution) protected readonly contributors: ContributionProvider; - protected nextId = 1; - protected readonly sessions = new Map(); - - async create(_contributionId: string, startParameters: any): Promise { - const id = this.nextId; - this.nextId++; - const sessionId = String(id); - this.sessions.set(sessionId, startParameters); - return sessionId; - } - - async destroy(sessionId: string): Promise { - this.sessions.delete(sessionId); - } - configure(service: MessagingService): void { for (const contribution of this.contributors.getContributions()) { const path = GLSPContribution.getPath(contribution); if (GLSPServerContributionOptions.shouldLaunchOnApplicationStart(contribution)) { - contribution.launch!().then(() => this.forward(service, path, contribution)); + contribution.launch?.().then(() => this.forward(service, path, contribution)); } else { this.forward(service, path, contribution); } @@ -56,12 +43,12 @@ export class GLSPBackendContribution implements MessagingService.Contribution, G } protected forward(service: MessagingService, path: string, contribution: GLSPServerContribution): void { - service.wsChannel(path, async (params, connection) => { + service.wsChannel(path, async (_params, clientChannel) => { try { - connection.onClose(() => this.destroy(params.id)); - await contribution.connect(connection); + clientChannel.onClose(() => contribution.dispose()); + await contribution.connect(clientChannel); } catch (e) { - this.logger.error(`Error occurred while starting GLSP contribution. ${path}.`, e); + console.error(`Error occurred while starting GLSP contribution. ${path}.`, e); } }); } diff --git a/packages/theia-integration/src/node/glsp-server-contribution.ts b/packages/theia-integration/src/node/glsp-server-contribution.ts index 0824b33..20ce1f9 100644 --- a/packages/theia-integration/src/node/glsp-server-contribution.ts +++ b/packages/theia-integration/src/node/glsp-server-contribution.ts @@ -16,15 +16,15 @@ /* eslint-disable indent */ import { MaybePromise } from '@eclipse-glsp/protocol'; -import { Channel } from '@theia/core'; +import { Channel, DisposableCollection } from '@theia/core'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { ProcessErrorEvent } from '@theia/process/lib/node/process'; import { ProcessManager } from '@theia/process/lib/node/process-manager'; import { RawProcess, RawProcessFactory } from '@theia/process/lib/node/raw-process'; import * as cp from 'child_process'; +import { createInterface } from 'readline'; import { GLSPContribution } from '../common'; import { ConnectionForwarder, IConnection } from './connection-forwarder'; - export const GLSPServerContribution = Symbol.for('GLSPServerContribution'); /** @@ -69,7 +69,7 @@ export namespace GLSPServerContributionOptions { return { launchOnDemand: false, launchedExternally: inDebugMode() - } as GLSPServerContributionOptions; + }; } /** @@ -78,12 +78,10 @@ export namespace GLSPServerContributionOptions { * @param options (partial) launch options that should be extended with default values (if necessary). */ export function configure(options?: Partial): GLSPServerContributionOptions { - return options - ? ({ - ...createDefaultOptions(), - ...options - } as GLSPServerContributionOptions) - : createDefaultOptions(); + return { + ...createDefaultOptions(), + ...options + }; } export const debugArgument = '--debug'; @@ -105,7 +103,7 @@ export namespace GLSPServerContributionOptions { * @returns `true` if the server should be launched on application start. */ export function shouldLaunchOnApplicationStart(contribution: GLSPServerContribution): boolean { - return contribution.launch !== undefined && !contribution.options.launchOnDemand && !contribution.options.launchedExternally; + return !contribution.options.launchOnDemand && !contribution.options.launchedExternally; } } @@ -124,11 +122,11 @@ export abstract class BaseGLSPServerContribution implements GLSPServerContributi abstract readonly id: string; options: GLSPServerContributionOptions; + protected toDispose = new DisposableCollection(); + @postConstruct() protected initialize(): void { - this.options = this.createContributionOptions - ? GLSPServerContributionOptions.configure(this.createContributionOptions()) - : GLSPServerContributionOptions.createDefaultOptions(); + this.options = GLSPServerContributionOptions.configure(this.createContributionOptions?.()); } abstract connect(clientChannel: Channel): MaybePromise; @@ -141,8 +139,10 @@ export abstract class BaseGLSPServerContribution implements GLSPServerContributi protected spawnProcessAsync(command: string, args?: string[], options?: cp.SpawnOptions): Promise { const rawProcess = this.processFactory({ command, args, options }); - rawProcess.errorStream.on('data', this.processLogError.bind(this)); - rawProcess.outputStream.on('data', this.processLogInfo.bind(this)); + + createInterface(rawProcess.outputStream).on('line', line => this.processLogInfo(line)); + createInterface(rawProcess.errorStream).on('line', line => this.processLogError(line)); + return new Promise((resolve, reject) => { rawProcess.onError((error: ProcessErrorEvent) => { this.onDidFailSpawnProcess(error); @@ -163,15 +163,19 @@ export abstract class BaseGLSPServerContribution implements GLSPServerContributi console.error(error); } - protected processLogError(data: string | Buffer): void { - if (data) { - console.error(`${this.id}: ${data}`); + protected processLogError(line: string): void { + if (line) { + console.error(`${this.id}: ${line}`); } } - protected processLogInfo(data: string | Buffer): void { - if (data) { - console.info(`${this.id}: ${data}`); + protected processLogInfo(line: string): void { + if (line) { + console.info(`${this.id}: ${line}`); } } + + dispose(): void { + this.toDispose.dispose(); + } } diff --git a/packages/theia-integration/src/node/glsp-socket-server-contribution.ts b/packages/theia-integration/src/node/glsp-socket-server-contribution.ts index 0603375..cc49d18 100644 --- a/packages/theia-integration/src/node/glsp-socket-server-contribution.ts +++ b/packages/theia-integration/src/node/glsp-socket-server-contribution.ts @@ -13,8 +13,9 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Channel, MaybePromise } from '@theia/core'; +import { Channel, Disposable, MaybePromise } from '@theia/core'; import { ForwardingChannel } from '@theia/core/lib/common/message-rpc/channel'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import { injectable, postConstruct } from '@theia/core/shared/inversify'; import { RawProcess } from '@theia/process/lib/node/raw-process'; import * as fs from 'fs'; @@ -49,7 +50,7 @@ export namespace GLSPSocketServerContributionOptions { socketConnectionOptions: { port: NaN } - } as GLSPSocketServerContributionOptions; + }; } /** @@ -58,12 +59,10 @@ export namespace GLSPSocketServerContributionOptions { * @param options (partial) launch options that should be extended with default values (if necessary) */ export function configure(options?: Partial): GLSPSocketServerContributionOptions { - return options - ? ({ - ...createDefaultOptions(), - ...options - } as GLSPSocketServerContributionOptions) - : createDefaultOptions(); + return { + ...createDefaultOptions(), + ...options + }; } } @@ -73,16 +72,12 @@ export namespace GLSPSocketServerContributionOptions { **/ @injectable() export abstract class GLSPSocketServerContribution extends BaseGLSPServerContribution { - protected resolveReady: (value?: void | PromiseLike | undefined) => void; - // eslint-disable-next-line no-invalid-this - onReady: Promise = new Promise(resolve => (this.resolveReady = resolve)); override options: GLSPSocketServerContributionOptions; + protected onReadyDeferred = new Deferred(); @postConstruct() protected override initialize(): void { - if (this.createContributionOptions) { - this.options = GLSPSocketServerContributionOptions.configure(this.createContributionOptions()); - } + this.options = GLSPSocketServerContributionOptions.configure(this.createContributionOptions()); } abstract override createContributionOptions(): Partial; @@ -92,27 +87,31 @@ export abstract class GLSPSocketServerContribution extends BaseGLSPServerContrib } async launch(): Promise { - if (!this.options.executable) { - throw new Error('Could not launch GLSP server. No executable path is provided via the contribution options'); - } - if (!fs.existsSync(this.options.executable)) { - throw new Error(`Could not launch GLSP server. The given server executable path is not valid: ${this.options.executable}`); - } - if (isNaN(this.options.socketConnectionOptions.port)) { - throw new Error( - `Could not launch GLSP Server. The given server port is not a number: ${this.options.socketConnectionOptions.port}` - ); - } + try { + if (!this.options.executable) { + throw new Error('Could not launch GLSP server. No executable path is provided via the contribution options'); + } + if (!fs.existsSync(this.options.executable)) { + throw new Error(`Could not launch GLSP server. The given server executable path is not valid: ${this.options.executable}`); + } + if (isNaN(this.options.socketConnectionOptions.port)) { + throw new Error( + `Could not launch GLSP Server. The given server port is not a number: ${this.options.socketConnectionOptions.port}` + ); + } - if (this.options.executable.endsWith('.jar')) { - await this.launchJavaProcess(); - } else if (this.options.executable.endsWith('.js')) { - await this.startNodeProcess(); - } else { - throw new Error(`Could not launch GLSP Server. Invalid executable path ${this.options.executable}`); + if (this.options.executable.endsWith('.jar')) { + await this.launchJavaProcess(); + } else if (this.options.executable.endsWith('.js')) { + await this.launchNodeProcess(); + } else { + throw new Error(`Could not launch GLSP Server. Invalid executable path ${this.options.executable}`); + } + } catch (error) { + this.onReadyDeferred.reject(error); } - return this.onReady; + return this.onReadyDeferred.promise; } protected launchJavaProcess(): Promise { @@ -138,7 +137,7 @@ export abstract class GLSPSocketServerContribution extends BaseGLSPServerContrib return ['--add-opens', 'java.base/java.util=ALL-UNNAMED']; } - protected startNodeProcess(): Promise { + protected launchNodeProcess(): Promise { const args = [this.options.executable!, '--port', `${this.options.socketConnectionOptions.port}`]; if (this.options.socketConnectionOptions.host) { @@ -151,11 +150,11 @@ export abstract class GLSPSocketServerContribution extends BaseGLSPServerContrib return this.spawnProcessAsync('node', args); } - protected override processLogInfo(data: string | Buffer): void { - if (data) { - const message = data.toString(); + protected override processLogInfo(line: string): void { + if (line) { + const message = line.toString(); if (message.startsWith(START_UP_COMPLETE_MSG)) { - this.resolveReady(); + this.onReadyDeferred.resolve(); } } } @@ -179,6 +178,7 @@ export abstract class GLSPSocketServerContribution extends BaseGLSPServerContrib socket.on('error', error => clientChannel.onErrorEmitter.fire(error)); } socket.connect(this.options.socketConnectionOptions); + this.toDispose.push(Disposable.create(() => socket.destroy())); } }