Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor+fix trace server url initialization #502

Merged
merged 13 commits into from
Sep 27, 2021
35 changes: 35 additions & 0 deletions packages/base/src/lazy-tsp-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { TspClient } from 'tsp-typescript-client';

/**
* Hack!
* The `LazyTspClient` replaces _every_ method with an asynchronous one.
* Only keep methods, discard properties.
*/
export type LazyTspClient = {
[K in keyof TspClient]: TspClient[K] extends (...args: infer A) => (infer R | Promise<infer R>)
? (...args: A) => Promise<R>
: never // Discard property.
};

export type LazyTspClientFactory = typeof LazyTspClientFactory;
export function LazyTspClientFactory(url: Promise<string>): TspClient {
// All methods from the `TspClient` are asynchronous. The `LazyTspClient`
// will just delay each call to its methods by first awaiting for the
// asynchronous `baseUrl` resolution to then get a valid `TspClient`.
const tspClientPromise = url.then(baseUrl => new TspClient(baseUrl));
// eslint-disable-next-line no-null/no-null
return new Proxy(Object.create(null), {
get(target, property, _receiver) {
let method = target[property];
if (!method) {
target[property] = method = async (...args: any[]) => {
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved
const tspClient = await tspClientPromise as any;
return tspClient[property](...args);
};
}
return method;
}
}) as LazyTspClient as TspClient;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export function bindTraceServerPreferences(bind: interfaces.Bind): void {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
return createPreferenceProxy(preferences, ServerSchema);
}).inSingletonScope();

bind(PreferenceContribution).toConstantValue({
schema: ServerSchema,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { PreferenceSchema, PreferenceProxy, PreferenceScope } from '@theia/core/lib/browser';
import { TRACE_SERVER_DEFAULT_PORT } from '../common/trace-server-url-provider';

export const TRACE_PATH = 'trace-viewer.path';
export const TRACE_PORT = 'trace-viewer.port';

export const ServerSchema: PreferenceSchema = {
scope: PreferenceScope.Folder,
type: 'object',
properties: {
[TRACE_PATH]: {
'type': 'string',
'default': '',
'description': 'The path to trace-server executable, e.g.: /usr/bin/tracecompass-server',
type: 'string',
default: '',
description: 'The path to trace-server executable, e.g.: /usr/bin/tracecompass-server',
},
[TRACE_PORT]: {
'type': 'number',
'default': '',
'description': 'Specify the port on which you want to execute the server. This change will take effect the next time you open a trace',
type: 'number',
default: TRACE_SERVER_DEFAULT_PORT,
description: 'Specify the port on which you want to execute the server. This change will take effect the next time you open a trace',
}

},
scope: PreferenceScope.Folder,
};

interface TracePreferenceContribution {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,66 +1,126 @@
import { Emitter, Event, MessageService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables/env-variables-protocol';
import { inject, injectable } from 'inversify';
import { TraceViewerEnvironment } from '../common/trace-viewer-environment';
import { TraceServerUrlProvider, TRACE_SERVER_DEFAULT_URL, TRACE_SERVER_DEFAULT_PORT } from '../common/trace-server-url-provider';
import { FrontendApplicationContribution, FrontendApplication, PreferenceService } from '@theia/core/lib/browser';
import { TRACE_PORT } from './trace-server-preference';
import { TraceServerConfigService } from '../common/trace-server-config';
import { MessageService } from '@theia/core';
import { TraceServerUrlProvider, TRACE_SERVER_DEFAULT_URL } from '../common/trace-server-url-provider';
import { TracePreferences, TRACE_PORT } from './trace-server-preference';

@injectable()
export class TraceServerUrlProviderImpl implements TraceServerUrlProvider, FrontendApplicationContribution {

protected _traceServerUrl: string;
protected _listeners: ((url: string) => void)[];
private port: string | undefined;
/**
* The Trace Server URL resolved from a URL template and a port number.
* Updated each time the port is changed from the preferences.
* `undefined` until both `_traceServerUrlTemplate` and `_traceServerPort` are initialized.
*/
protected _traceServerUrl?: string;

constructor(
@inject(TraceViewerEnvironment) protected readonly traceViewerEnvironment: TraceViewerEnvironment,
@inject(PreferenceService) protected readonly preferenceService: PreferenceService,
@inject(TraceServerConfigService) protected readonly traceServerConfigService: TraceServerConfigService,
@inject(MessageService) protected readonly messageService: MessageService
/**
* The Trace Server URL template.
* The `{}` characters will be replaced with the port defined in the preferences.
* `undefined` until fetched from the remote environment.
*/
protected _traceServerUrlTemplate?: string;

) {
this.port = this.preferenceService.get(TRACE_PORT);
this.preferenceService.onPreferenceChanged(async event => {
if (event.preferenceName === TRACE_PORT) {
try {
await this.traceServerConfigService.stopTraceServer();
this.messageService.info(`Trace server disconnected on port: ${this.port}.`);
} catch (e){
// Do not show the error incase the user tries to modify the port before starting a server
}
this.port = event.newValue;
this._traceServerUrl = TRACE_SERVER_DEFAULT_URL.replace(/{}/g, this.port ? this.port : TRACE_SERVER_DEFAULT_PORT);
this.updateListeners();
/**
* A configurable port number from the preferences.
* `undefined` until fetched from the preferences.
*/
protected _traceServerPort?: number;

}
/**
* Internal promise that is pending until `_traceServerUrl` is initialized.
* After then, each update of the Trace Server URL will create a new reference
* to a promise resolved with the new value.
*/
protected _traceServerUrlPromise: Promise<string>;

});
protected _onDidChangeTraceServerUrlEmitter = new Emitter<string>();

this._traceServerUrl = TRACE_SERVER_DEFAULT_URL.replace(/{}/g, this.port ? this.port : TRACE_SERVER_DEFAULT_PORT);
this._listeners = [];
/**
* Listen for updates to the Trace Server URL.
* Fired when Trace Server URL is first initiliazed and when the `TRACE_PORT` preference changes.
*/
get onDidChangeTraceServerUrl(): Event<string> {
return this._onDidChangeTraceServerUrlEmitter.event;
}

constructor(
@inject(EnvVariablesServer) protected environment: EnvVariablesServer,
@inject(TracePreferences) protected tracePreferences: TracePreferences,
@inject(TraceServerConfigService) protected traceServerConfigService: TraceServerConfigService,
@inject(MessageService) protected messageService: MessageService,
) {
this._traceServerUrlPromise = new Promise(resolve => {
const self = this.onDidChangeTraceServerUrl(url => {
self.dispose();
resolve(url);
});
});
// Get the URL template from the remote environment.
this.environment.getValue('TRACE_SERVER_URL').then(variable => {
const url = variable?.value;
this._traceServerUrlTemplate = url
? this.normalizeUrl(url)
: TRACE_SERVER_DEFAULT_URL;
this.updateTraceServerUrl();
});
// Get the configurable port from Theia's preferences.
this.tracePreferences.ready.then(() => {
this._traceServerPort = this.tracePreferences[TRACE_PORT];
this.updateTraceServerUrl();
this.tracePreferences.onPreferenceChanged(async event => {
if (event.preferenceName === TRACE_PORT) {
this._traceServerPort = event.newValue;
this.updateTraceServerUrl();
try {
await this.traceServerConfigService.stopTraceServer();
this.messageService.info(`Trace server disconnected on port: ${event.oldValue}.`);
} catch (_) {
// Do not show the error incase the user tries to modify the port before starting a server
}
}
});
});
}

async onStart(_app: FrontendApplication): Promise<void> {
this._traceServerUrl = await this.traceViewerEnvironment.getTraceServerUrl();
this.updateListeners();
async initialize(): Promise<void> {
// Don't start the application until the Trace Server URL is initialized.
await this._traceServerUrlPromise;
}

async updateListeners(): Promise<void> {
this._listeners.forEach(listener => listener(this._traceServerUrl));
async getTraceServerUrlPromise(): Promise<string> {
return this._traceServerUrlPromise;
}

getTraceServerUrl(): string {
if (this._traceServerUrl === undefined) {
throw new Error('The Trace Server URL is not yet defined. Try using getTraceServerUrlPromise.');
}
return this._traceServerUrl;
}

/**
* Add a listener for trace server url changes
* @param listener The listener function to be called when the url is
* changed
*/
addTraceServerUrlChangedListener(listener: (url: string) => void): void {
this._listeners.push(listener);
protected normalizeUrl(url: string): string {
url = url.toLowerCase();
// Add missing http protocol.
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
// Remove trailing `/`.
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}
return url;
}

protected updateTraceServerUrl(): void {
if (this._traceServerUrlTemplate === undefined || this._traceServerPort === undefined) {
return; // State is only partially initialized = try again later.
}
const traceServerUrl = this._traceServerUrlTemplate.replace(/{}/g, this._traceServerPort.toString());
this._traceServerUrl = traceServerUrl;
this._traceServerUrlPromise = Promise.resolve(traceServerUrl);
this._onDidChangeTraceServerUrlEmitter.fire(traceServerUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ContainerModule, Container } from 'inversify';
import { WidgetFactory, OpenHandler, FrontendApplicationContribution, bindViewContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser';
import { TraceViewerWidget, TraceViewerWidgetOptions } from './trace-viewer';
import { TraceViewerContribution } from './trace-viewer-contribution';
import { TraceViewerEnvironment } from '../../common/trace-viewer-environment';
import { TraceServerUrlProvider } from '../../common/trace-server-url-provider';
import { CommandContribution } from '@theia/core/lib/common';
import 'ag-grid-community/dist/styles/ag-grid.css';
Expand All @@ -19,12 +18,13 @@ import { bindTraceServerPreferences } from '../trace-server-bindings';
import { TraceServerConfigService, traceServerPath } from '../../common/trace-server-config';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { TraceViewerToolbarContribution } from './trace-viewer-toolbar-contribution';
import { LazyTspClientFactory } from 'traceviewer-base/lib/lazy-tsp-client';

export default new ContainerModule(bind => {
bind(TraceViewerEnvironment).toSelf().inRequestScope();
bind(TraceServerUrlProviderImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(TraceServerUrlProviderImpl);
bind(TraceServerUrlProvider).toService(TraceServerUrlProviderImpl);
bind(LazyTspClientFactory).toFunction(LazyTspClientFactory);
bind(TspClientProvider).toSelf().inSingletonScope();
bind(TheiaMessageManager).toSelf().inSingletonScope();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TspClient } from 'tsp-typescript-client/lib/protocol/tsp-client';
import { ExperimentManager } from 'traceviewer-base/lib/experiment-manager';
import { TraceManager } from 'traceviewer-base/lib/trace-manager';
import { ITspClientProvider } from 'traceviewer-base/lib/tsp-client-provider';
import { LazyTspClientFactory } from 'traceviewer-base/lib/lazy-tsp-client';

@injectable()
export class TspClientProvider implements ITspClientProvider {
Expand All @@ -14,17 +15,24 @@ export class TspClientProvider implements ITspClientProvider {
private _listeners: ((tspClient: TspClient) => void)[];

constructor(
@inject(TraceServerUrlProvider) private tspUrlProvider: TraceServerUrlProvider
@inject(TraceServerUrlProvider) private tspUrlProvider: TraceServerUrlProvider,
@inject(LazyTspClientFactory) private lazyTspClientFactory: LazyTspClientFactory,
) {
this._tspClient = new TspClient(this.tspUrlProvider.getTraceServerUrl());
const traceServerUrlPromise = this.tspUrlProvider.getTraceServerUrlPromise();
this._tspClient = this.lazyTspClientFactory(traceServerUrlPromise);
this._traceManager = new TraceManager(this._tspClient);
this._experimentManager = new ExperimentManager(this._tspClient, this._traceManager);
this._listeners = [];
tspUrlProvider.addTraceServerUrlChangedListener(url => {
this._tspClient = new TspClient(url);
this._traceManager = new TraceManager(this._tspClient);
this._experimentManager = new ExperimentManager(this._tspClient, this._traceManager);
this._listeners.forEach(listener => listener(this._tspClient));
// Skip the first event fired when the Trace Server URL gets initialized.
traceServerUrlPromise.then(() => {
tspUrlProvider.onDidChangeTraceServerUrl(url => {
this._tspClient = new TspClient(url);
this._traceManager = new TraceManager(this._tspClient);
this._experimentManager = new ExperimentManager(this._tspClient, this._traceManager);
for (const listener of this._listeners) {
listener(this._tspClient);
}
});
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
export const TraceServerUrlProvider = Symbol('TraceServerUrlProvider');
export const TRACE_SERVER_DEFAULT_URL = 'http://localhost:{}/tsp/api';
export const TRACE_SERVER_DEFAULT_PORT = '8080';

export const TraceServerUrlProvider = Symbol('TraceServerUrlProvider');
export interface TraceServerUrlProvider {

/**
* Get the default trace server URL from the server
* Get a promise that resolves once the Trace Server URL is initialized.
* @returns a new promise each time `.onDidChangeTraceServerUrl` fires.
*/
getTraceServerUrl(): Readonly<string>;
getTraceServerUrlPromise(): Promise<string>;

/**
* Add a listener for trace server url changes
* @param listener The listener function to be called when the url is
* changed
* Get the default Trace Server URL from the server.
* Will throw if called before initialization. See `getTraceServerUrlPromise`
* to get a promise to the value.
*/
addTraceServerUrlChangedListener(listener: (url: string) => void): void;
getTraceServerUrl(): string;

/**
* Get notified when the Trace Server URL changes.
* @param listener function to be called when the url is changed.
*/
onDidChangeTraceServerUrl(listener: (url: string) => void): void;
}

This file was deleted.