diff --git a/.vscode/settings.json b/.vscode/settings.json index eef0b9cb..da5515c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,7 @@ "**/build/azure-pipelines/**/*.yml": "azure-pipelines" }, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": ["javascript", "typescript"], diff --git a/extension/src/client/command/DeletePackage.ts b/extension/src/client/command/DeletePackage.ts index 2d18c55d..665ee657 100644 --- a/extension/src/client/command/DeletePackage.ts +++ b/extension/src/client/command/DeletePackage.ts @@ -26,7 +26,7 @@ export class DeletePackage extends Command { constructor(config: ConfigurationManager, environment: EnvironmentManager) { super() - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } async execute(context: vscode.ExtensionContext, node: PackageNode): Promise { diff --git a/extension/src/client/command/FetchWorkflowSchema.ts b/extension/src/client/command/FetchWorkflowSchema.ts index 2e11a507..df631eb3 100644 --- a/extension/src/client/command/FetchWorkflowSchema.ts +++ b/extension/src/client/command/FetchWorkflowSchema.ts @@ -25,7 +25,7 @@ export class FetchWorkflowSchema extends Command { constructor(config: ConfigurationManager, environment: EnvironmentManager) { super() - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } async execute(context: vscode.ExtensionContext, node: WorkflowNode): Promise { diff --git a/extension/src/client/command/RunAction.ts b/extension/src/client/command/RunAction.ts index 4feb246a..0973fc6d 100644 --- a/extension/src/client/command/RunAction.ts +++ b/extension/src/client/command/RunAction.ts @@ -147,53 +147,17 @@ export class RunAction extends Command { } private async getScriptContent(document: vscode.TextDocument): Promise { - if (document.languageId === "javascript") { - return document.getText() - } - - if (document.languageId === "typescript") { - let inputFilePath = document.uri.fsPath - let inputFileName = path.basename(inputFilePath) - let tsNamespace: string | undefined - let rootPath: string - let srcPath: string - - if (!document.isUntitled) { - const workspacePath = vscode.workspace.getWorkspaceFolder(document.uri) - if (!workspacePath) { - throw new Error(`File ${inputFileName} is not part of the workspace`) - } - - rootPath = workspacePath.uri.fsPath - srcPath = path.join(rootPath, "src") - const pomFilePath = path.join(workspacePath.uri.fsPath, "pom.xml") - - if (!fs.existsSync(pomFilePath)) { - throw new Error(`Missing pom.xml in workspace ${workspacePath.name}`) - } - - const pomFile = new PomFile(pomFilePath) - tsNamespace = `${pomFile.groupId}.${pomFile.artifactId}` - } else { - rootPath = tmp.dirSync({ prefix: "o11n-ts-" }).name - srcPath = path.join(rootPath, "src") - inputFileName = inputFileName.endsWith(".ts") ? inputFileName : `${inputFileName}.ts` - inputFilePath = path.join(srcPath, inputFileName) - fs.mkdirpSync(path.dirname(inputFilePath)) - fs.writeFileSync(inputFilePath, document.getText(), { encoding: "utf8" }) + switch (document.languageId) { + case "javascript": { + return document.getText() + } + case "typescript": { + return this.getTypescriptContent(document) + } + default: { + return Promise.reject(new Error(`Unsupported language ID: '${document.languageId}'`)) } - - this.outputChannel.appendLine(`# Compiling ${inputFileName}`) - const tsFileRelativePath = path.relative(srcPath, inputFilePath) - this.logger.debug(`Input TS file: ${inputFilePath}`) - const outputFilePath = await this.compileFile(tsFileRelativePath, rootPath, tsNamespace) - this.logger.debug(`Output JS file: ${outputFilePath}`) - const scriptContent = fs.readFileSync(outputFilePath, { encoding: "utf8" }) - - return scriptContent } - - return Promise.reject(`Unsupported language ID: ${document.languageId}`) } /** @@ -213,8 +177,55 @@ export class RunAction extends Command { command += ` -n ${namespace}` } await proc.exec(command, { cwd: projectDirPath }, this.logger) + return path.join(outputDir, inputFile.replace(/\.ts$/, ".js")) } + + /** + * Return the typescript content of a vscode text document. + * @param document - reference to the vscode document. + * + * @returns the compiled javascript from the typescript document. + */ + private async getTypescriptContent(document: vscode.TextDocument): Promise { + let inputFilePath = document.uri.fsPath + let inputFileName = path.basename(inputFilePath) + let tsNamespace: string | undefined + let rootPath: string + let srcPath: string + + if (!document.isUntitled) { + const workspacePath = vscode.workspace.getWorkspaceFolder(document.uri) + if (!workspacePath) { + throw new Error(`File ${inputFileName} is not part of the workspace`) + } + rootPath = workspacePath.uri.fsPath + srcPath = path.join(rootPath, "src") + const pomFilePath = path.join(workspacePath.uri.fsPath, "pom.xml") + if (!fs.existsSync(pomFilePath)) { + throw new Error(`Missing pom.xml in workspace ${workspacePath.name}`) + } + + const pomFile = new PomFile(pomFilePath) + tsNamespace = `${pomFile.groupId}.${pomFile.artifactId}` + } else { + rootPath = tmp.dirSync({ prefix: "o11n-ts-" }).name + srcPath = path.join(rootPath, "src") + inputFileName = inputFileName.endsWith(".ts") ? inputFileName : `${inputFileName}.ts` + inputFilePath = path.join(srcPath, inputFileName) + fs.mkdirpSync(path.dirname(inputFilePath)) + fs.writeFileSync(inputFilePath, document.getText(), { encoding: "utf8" }) + } + + this.outputChannel.appendLine(`# Compiling ${inputFileName}`) + const tsFileRelativePath = path.relative(srcPath, inputFilePath) + this.logger.debug(`Input TS file: ${inputFilePath}`) + const outputFilePath = await this.compileFile(tsFileRelativePath, rootPath, tsNamespace) + this.logger.debug(`Output JS file: ${outputFilePath}`) + const scriptContent = fs.readFileSync(outputFilePath, { encoding: "utf8" }) + + return scriptContent + } } class ActionRunner { @@ -225,7 +236,7 @@ class ActionRunner { private executionToken: string constructor(config: ConfigurationManager, private environment: EnvironmentManager) { - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) this.mavenProxy = new MavenCliProxy(environment, config.vrdev.maven, this.logger) } @@ -296,7 +307,7 @@ class ActionRunner { if (!fs.existsSync(storagePath)) { fs.mkdirSync(storagePath) } - + // exec await this.mavenProxy.copyDependency( "com.vmware.pscoe.o11n", "exec", diff --git a/extension/src/client/command/ShowActions.ts b/extension/src/client/command/ShowActions.ts index 192b51f4..80b6e0c6 100644 --- a/extension/src/client/command/ShowActions.ts +++ b/extension/src/client/command/ShowActions.ts @@ -22,7 +22,7 @@ export class ShowActions extends Command { constructor(environment: EnvironmentManager, private config: ConfigurationManager) { super() - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } async execute(context: vscode.ExtensionContext): Promise { diff --git a/extension/src/client/command/ShowConfigurations.ts b/extension/src/client/command/ShowConfigurations.ts index 21f75cbd..21bcd403 100644 --- a/extension/src/client/command/ShowConfigurations.ts +++ b/extension/src/client/command/ShowConfigurations.ts @@ -22,7 +22,7 @@ export class ShowConfigurations extends Command { constructor(environment: EnvironmentManager, config: ConfigurationManager) { super() - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } async execute(context: vscode.ExtensionContext): Promise { diff --git a/extension/src/client/command/ShowResources.ts b/extension/src/client/command/ShowResources.ts index 93527eb0..c68e8cef 100644 --- a/extension/src/client/command/ShowResources.ts +++ b/extension/src/client/command/ShowResources.ts @@ -22,7 +22,7 @@ export class ShowResources extends Command { constructor(environment: EnvironmentManager, config: ConfigurationManager) { super() - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } async execute(context: vscode.ExtensionContext): Promise { diff --git a/extension/src/client/command/ShowWorkflows.ts b/extension/src/client/command/ShowWorkflows.ts index 966521d0..67727907 100644 --- a/extension/src/client/command/ShowWorkflows.ts +++ b/extension/src/client/command/ShowWorkflows.ts @@ -22,7 +22,7 @@ export class ShowWorkflows extends Command { constructor(environment: EnvironmentManager, config: ConfigurationManager) { super() - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } async execute(context: vscode.ExtensionContext): Promise { diff --git a/extension/src/client/command/TriggerCollection.ts b/extension/src/client/command/TriggerCollection.ts index 959c639e..23c8352a 100644 --- a/extension/src/client/command/TriggerCollection.ts +++ b/extension/src/client/command/TriggerCollection.ts @@ -6,8 +6,10 @@ import { AutoWire, Logger, sleep } from "@vmware/vrdt-common" import { remote } from "@vmware/vro-language-server" import * as vscode from "vscode" +import { LanguageClient } from "vscode-languageclient" +import { CollectionStatus } from "packages/node/vro-language-server/src/server/request/collection/ServerCollection" -import { Commands } from "../constants" +import { Commands, LanguageServerConfig } from "../constants" import { LanguageServices } from "../lang" import { Command } from "./Command" @@ -30,10 +32,9 @@ export class TriggerCollection extends Command { const languageClient = this.languageServices.client if (!languageClient) { - this.logger.warn("The vRO language server is not running") + this.logger.warn(`The ${LanguageServerConfig.DisplayName} is not running`) return } - await vscode.commands.executeCommand(Commands.EventCollectionStart) vscode.window.withProgress( @@ -41,35 +42,36 @@ export class TriggerCollection extends Command { location: vscode.ProgressLocation.Window, title: "vRO hint collection" }, - progress => { + async progress => { return new Promise(async (resolve, reject) => { - await languageClient.sendRequest(remote.server.triggerVroCollection, false) - let status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) - - while (status && !status.finished) { - this.logger.info("Collection status:", status) - progress.report(status) - await sleep(1000) - status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) - } - - this.logger.info("Collection finished:", status) - + const status: CollectionStatus = await this.triggerVroDataCollection(languageClient, progress) if (status.error !== undefined) { - await vscode.commands.executeCommand(Commands.EventCollectionError, status.error) - - if (status.data.hintsPluginBuild === 0) { - vscode.window.showErrorMessage( - "The vRO Hint plug-in is not installed on the configured vRO server" - ) - } - } else { - await vscode.commands.executeCommand(Commands.EventCollectionSuccess) + reject(new Error(`Failed to trigger data collection from vRO: ${status.error}`)) } - resolve() }) } ) } + + private async triggerVroDataCollection(languageClient: LanguageClient, progress: any): Promise { + await languageClient.sendRequest(remote.server.triggerVroCollection, false) + let status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) + + // wait for status change + while (status && !status.finished) { + this.logger.info("Collection status:", status) + progress.report(status) + await sleep(LanguageServerConfig.SleepTime) + status = await languageClient.sendRequest(remote.server.giveVroCollectionStatus) + } + // check for error response + if (status.error !== undefined) { + await vscode.commands.executeCommand(Commands.EventCollectionError, status.error) + return status + } + await vscode.commands.executeCommand(Commands.EventCollectionSuccess) + + return status + } } diff --git a/extension/src/client/constants.ts b/extension/src/client/constants.ts index f3056f6d..77bf7a57 100644 --- a/extension/src/client/constants.ts +++ b/extension/src/client/constants.ts @@ -110,3 +110,11 @@ export enum ProjectArchetypes { VraNg = "com.vmware.pscoe.vra-ng:vra-ng-package", Polyglot = "com.vmware.pscoe.polyglot:polyglot-project" } + +export enum LanguageServerConfig { + Port = 6014, + LoadType = "nolazy", + NodeType = "node-ipc", + DisplayName = "vRO Language Server", + SleepTime = 1000 +} diff --git a/extension/src/client/lang/LanguageServices.ts b/extension/src/client/lang/LanguageServices.ts index 51709e6f..fff3bb7c 100644 --- a/extension/src/client/lang/LanguageServices.ts +++ b/extension/src/client/lang/LanguageServices.ts @@ -10,7 +10,7 @@ import { remote } from "@vmware/vro-language-server" import * as vscode from "vscode" import * as client from "vscode-languageclient" -import { OutputChannels } from "../constants" +import { LanguageServerConfig, OutputChannels } from "../constants" import { Registrable } from "../Registrable" import { ConfigurationManager, EnvironmentManager } from "../system" @@ -82,20 +82,23 @@ export class LanguageServices implements Registrable, vscode.Disposable { ? path.join(module, "dist", "langserver.js") : path.join(module, "out", "server", "langserver.js") - this.logger.info(`Starting vRO language server on port 6014`) + this.logger.info(`Starting vRO language server on port '${LanguageServerConfig.Port}'`) const serverOptions = { run: { module: executable, transport: client.TransportKind.ipc, - args: ["--node-ipc"], + args: [`--${LanguageServerConfig.NodeType}`], options: { cwd: module } }, debug: { module: executable, transport: client.TransportKind.ipc, - args: ["--node-ipc"], - options: { cwd: module, execArgv: ["--nolazy", "--inspect=6014"] } + args: [`--${LanguageServerConfig.NodeType}`], + options: { + cwd: module, + execArgv: [`--${LanguageServerConfig.LoadType}`, `--inspect=${LanguageServerConfig.Port}`] + } } } @@ -119,6 +122,6 @@ export class LanguageServices implements Registrable, vscode.Disposable { ] } } - return new client.LanguageClient("vRO LS", serverOptions, clientOptions) + return new client.LanguageClient(LanguageServerConfig.DisplayName, serverOptions, clientOptions) } } diff --git a/extension/src/client/provider/content/ContentProvider.ts b/extension/src/client/provider/content/ContentProvider.ts index 449b4d95..22d74bd7 100644 --- a/extension/src/client/provider/content/ContentProvider.ts +++ b/extension/src/client/provider/content/ContentProvider.ts @@ -24,7 +24,7 @@ export class ContentProvider implements vscode.TextDocumentContentProvider, Regi private readonly logger = Logger.get("ContentProvider") constructor(environment: EnvironmentManager, config: ConfigurationManager) { - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } dispose() { diff --git a/extension/src/client/provider/explorer/ExplorerProvider.ts b/extension/src/client/provider/explorer/ExplorerProvider.ts index 8d3bfcb5..c885e95f 100644 --- a/extension/src/client/provider/explorer/ExplorerProvider.ts +++ b/extension/src/client/provider/explorer/ExplorerProvider.ts @@ -33,7 +33,7 @@ export class ExplorerProvider implements vscode.TreeDataProvider, readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEmitter.event constructor(environment: EnvironmentManager, private config: ConfigurationManager) { - this.restClient = new VroRestClient(config, environment) + this.restClient = new VroRestClient(config) } register(context: vscode.ExtensionContext): void { diff --git a/extension/src/client/system/ConfigurationManager.ts b/extension/src/client/system/ConfigurationManager.ts index 72d26dbc..0cd60dcb 100644 --- a/extension/src/client/system/ConfigurationManager.ts +++ b/extension/src/client/system/ConfigurationManager.ts @@ -23,7 +23,7 @@ import { Registrable } from "../Registrable" @AutoWire export class ConfigurationManager extends BaseConfiguration implements Registrable { - private homeDir = process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"] || "~" + private homeDir = process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"] ?? "~" private readonly logger = Logger.get("ConfigurationManager") readonly settingsXmlPath: string = path.resolve(this.homeDir, ".m2", "settings.xml") diff --git a/extension/src/client/ui/StatusBarController.ts b/extension/src/client/ui/StatusBarController.ts index 47c77342..645ebfd6 100644 --- a/extension/src/client/ui/StatusBarController.ts +++ b/extension/src/client/ui/StatusBarController.ts @@ -53,14 +53,6 @@ export class StatusBarController implements Registrable, vscode.Disposable { this.collectionStatus.dispose() } - private onConfigurationOrProfilesChanged() { - const currentProfileName = this.config.hasActiveProfile() ? this.config.activeProfile.get("id") : undefined - - if (this.verifyConfiguration() && currentProfileName !== this.profileName && this.env.hasRelevantProject()) { - vscode.commands.executeCommand(Commands.TriggerServerCollection) - } - } - verifyConfiguration(): boolean { this.profileName = this.config.hasActiveProfile() ? this.config.activeProfile.get("id") : undefined this.logger.info(`Verifying configuration for active profile ${this.profileName}`) @@ -96,6 +88,14 @@ export class StatusBarController implements Registrable, vscode.Disposable { return false } + private onConfigurationOrProfilesChanged() { + const currentProfileName = this.config.hasActiveProfile() ? this.config.activeProfile.get("id") : undefined + + if (this.verifyConfiguration() && currentProfileName !== this.profileName && this.env.hasRelevantProject()) { + vscode.commands.executeCommand(Commands.TriggerServerCollection) + } + } + private onCollectionStart() { this.collectionButton.text = "$(watch) " this.collectionButton.command = Commands.TriggerServerCollection diff --git a/package.json b/package.json index 84721161..fa5ce75c 100644 --- a/package.json +++ b/package.json @@ -521,6 +521,12 @@ ], "markdownDescription": "Specifies how the _Explorer_ view will display vRO action packages", "scope": "window" + }, + "vrdev.vro.inventory.cache": { + "type": "boolean", + "default": true, + "description": "Enable vRO inventory caching (can be used in heavily loaded environments)", + "scope": "window" } } }, @@ -620,4 +626,4 @@ "preinstall": "node build/preinstall.js", "vscode:prepublish": "webpack --mode production --config ./extension/webpack.config.js && webpack --mode production --config ./packages/node/vro-language-server/webpack.config.js" } -} +} \ No newline at end of file diff --git a/packages/node/vrdt-common/src/rest/VroRestClient.ts b/packages/node/vrdt-common/src/rest/VroRestClient.ts index 5a455151..10f4155b 100644 --- a/packages/node/vrdt-common/src/rest/VroRestClient.ts +++ b/packages/node/vrdt-common/src/rest/VroRestClient.ts @@ -9,28 +9,42 @@ import * as fs from "fs-extra" import * as request from "request-promise-native" import { ApiCategoryType } from "../types" -import { BaseConfiguration, BaseEnvironment } from "../platform" +import { BaseConfiguration } from "../platform" import { Auth, BasicAuth, VraSsoAuth } from "./auth" import { + Action, ApiElement, + Configuration, ContentChildrenResponse, ContentLinksResponse, InventoryElement, + LinkItem, LogMessage, + Resource, Version, + Workflow, WorkflowLogsResponse, WorkflowParam, WorkflowState } from "./vro-model" -import { Logger, MavenCliProxy, promise, sleep } from ".." +import { Logger, promise, sleep } from ".." import { HintAction, HintModule } from "../types/hint" export class VroRestClient { private readonly logger = Logger.get("VroRestClient") - private auth - - constructor(private settings: BaseConfiguration, private environment: BaseEnvironment) { + private auth: Record | PromiseLike> + private hintActions: Map = new Map() + private rootElements: Map = new Map() + private childElements: Map = new Map() + private actions: Action[] = [] + private workflows: Workflow[] = [] + private configurations: Configuration[] = [] + private resources: Resource[] = [] + private isCachingEnabled: boolean + + constructor(private settings: BaseConfiguration) { this.auth = this.getInitialAuth() + this.loadCacheConfiguration() } private get hostname(): string { @@ -73,14 +87,12 @@ export class VroRestClient { this.logger.info("Initial authentication...") let auth: Auth - await sleep(1000) // to properly initialize the components + await sleep(3000) // to properly initialize the components let refreshToken = this.refreshToken - switch (this.authMethod.toLowerCase()) { case "vra": this.logger.info(`Token authentication chosen...`) - new MavenCliProxy(this.environment, this.settings.vrdev.maven, this.logger) if (this.vroUsername && this.vroPassword) { refreshToken = await this.getRefreshToken(this.vroUsername, this.vroPassword) } @@ -94,6 +106,7 @@ export class VroRestClient { default: throw new Error(`Unsupported authentication mechanism: ${this.authMethod}`) } + return auth.toRequestJson() } @@ -124,6 +137,7 @@ export class VroRestClient { } const bearerToken = await request(options) this.logger.debug(`Bearer token: ${bearerToken.token}`) + return bearerToken.token } @@ -159,6 +173,7 @@ export class VroRestClient { uri } const refreshToken = await request(options) + return refreshToken.refresh_token } @@ -168,6 +183,8 @@ export class VroRestClient { options?: Partial ): Promise { const url = route.indexOf("://") > 0 ? route : `https://${this.hostname}:${this.port}/vco/api/${route}` + // reload the cache configuration in order cache to take effect when settings are changed + this.loadCacheConfiguration() return request({ headers: { "Accept": "application/json", @@ -204,7 +221,6 @@ export class VroRestClient { async executeWorkflow(id: string, ...inputParams: WorkflowParam[]): Promise { const token: string = await this.startWorkflow(id, ...inputParams) let response = await this.getWorkflowExecution(id, token) - while (response.state === "running") { await sleep(1000) response = await this.getWorkflowExecution(id, token) @@ -221,6 +237,7 @@ export class VroRestClient { const response = await this.send("GET", `workflows/${id}/executions/${token}`, { resolveWithFullResponse: false }) + return response.state } @@ -239,9 +256,7 @@ export class VroRestClient { }) } } - const execResponse = await this.send("POST", `workflows/${id}/executions`, executeOptions) - if (execResponse.statusCode !== 202) { throw new Error(`Expected status code 202, but got ${execResponse.statusCode}`) } @@ -250,9 +265,9 @@ export class VroRestClient { if (!location) { throw new Error(`Missing location header in the response of POST /workflows/${id}/executions`) } - location = location.replace(/\/$/, "") // remove trailing slash const execToken = location.substring(location.lastIndexOf("/") + 1) + return execToken } @@ -276,11 +291,9 @@ export class VroRestClient { ) const messages: LogMessage[] = [] - for (const log of response.logs) { const e = log.entry const description = e["long-description"] ? e["long-description"] : e["short-description"] - if ( e.origin === "server" || // skip server messages, as they are always included in the result description.indexOf("*** End of execution stack.") > 0 || @@ -288,7 +301,6 @@ export class VroRestClient { ) { continue } - messages.push({ timestamp: e["time-stamp"], severity: e.severity, @@ -314,14 +326,12 @@ export class VroRestClient { ) const messages: LogMessage[] = [] - for (const log of response.logs) { const e = log.entry const description = e["long-description"] ? e["long-description"] : e["short-description"] if (description.indexOf("*** End of execution stack.") > 0 || description.startsWith("__item_stack:/")) { continue } - messages.push({ timestamp: e["time-stamp"], severity: e.severity, @@ -363,17 +373,21 @@ export class VroRestClient { }) .filter(val => val !== undefined) as string[] - return packages.sort() + return packages.sort((x, y) => x.localeCompare(y)) } - async getActions(): Promise<{ fqn: string; id: string; version: string }[]> { + async getActions(): Promise { + // return the cached actions (if any) if caching is enabled + if (this.isCachingEnabled && this.actions && Array.isArray(this.actions) && this.actions.length) { + return this.actions + } + const responseJson: ContentLinksResponse = await this.send("GET", "actions") const actions: { fqn: string; id: string; version: string }[] = responseJson.link .map(action => { if (!action.attributes) { return undefined } - const fqn = action.attributes.find(att => att.name === "fqn") const id = action.attributes.find(att => att.name === "id") const version = action.attributes.find(att => att.name === "version") @@ -388,10 +402,21 @@ export class VroRestClient { return !!val && val.fqn !== undefined && val.id !== undefined }) as { fqn: string; id: string; version: string }[] - return actions.sort((x, y) => x.fqn.localeCompare(y.fqn)) + actions.sort((x, y) => x.fqn.localeCompare(y.fqn)) + // cache the actions in order not to overload vRO if caching is enabled + if (this.isCachingEnabled) { + this.actions = actions + } + + return this.actions } - async getWorkflows(): Promise<{ name: string; id: string; version: string }[]> { + async getWorkflows(): Promise { + // return the cached workflows (if any) if caching is enabled + if (this.isCachingEnabled && this.workflows && Array.isArray(this.workflows) && this.workflows.length) { + return this.workflows + } + const responseJson: ContentLinksResponse = await this.send("GET", "workflows") const workflows: { name: string; id: string; version: string }[] = responseJson.link .map(wf => { @@ -413,17 +438,32 @@ export class VroRestClient { return !!val && val.name !== undefined && val.id !== undefined }) as { name: string; id: string; version: string }[] - return workflows.sort((x, y) => x.name.localeCompare(y.name)) + workflows.sort((x, y) => x.name.localeCompare(y.name)) + // cache the workflows if caching is enabled + if (this.isCachingEnabled) { + this.workflows = workflows + } + + return workflows } - async getConfigurations(): Promise<{ name: string; id: string; version: string }[]> { + async getConfigurations(): Promise { + // return the cached configurations (if any) if caching is enabled + if ( + this.isCachingEnabled && + this.configurations && + Array.isArray(this.configurations) && + this.configurations.length + ) { + return this.configurations + } + const responseJson: ContentLinksResponse = await this.send("GET", "configurations") const configs: { name: string; id: string; version: string }[] = responseJson.link .map(conf => { if (!conf.attributes) { return undefined } - const name = conf.attributes.find(att => att.name === "name") const id = conf.attributes.find(att => att.name === "id") const version = conf.attributes.find(att => att.name === "version") @@ -437,18 +477,28 @@ export class VroRestClient { .filter(val => { return !!val && val.name !== undefined && val.id !== undefined }) as { name: string; id: string; version: string }[] + configs.sort((x, y) => x.name.localeCompare(y.name)) + + // cache the configurations if caching is enabled + if (this.isCachingEnabled) { + this.configurations = configs + } - return configs.sort((x, y) => x.name.localeCompare(y.name)) + return configs } - async getResources(): Promise<{ name: string; id: string }[]> { + async getResources(): Promise { + // return the cached resources (if any) if caching is enabled + if (this.isCachingEnabled && this.resources && Array.isArray(this.resources) && this.resources.length) { + return this.resources + } + const responseJson: ContentLinksResponse = await this.send("GET", "resources") const resources: { name: string; id: string }[] = responseJson.link .map(res => { if (!res.attributes) { return undefined } - const name = res.attributes.find(att => att.name === "name") const id = res.attributes.find(att => att.name === "id") @@ -461,40 +511,72 @@ export class VroRestClient { return !!val && val.name !== undefined && val.id !== undefined }) as { name: string; id: string }[] - return resources.sort((x, y) => x.name.localeCompare(y.name)) + resources.sort((x, y) => x.name.localeCompare(y.name)) + + // cache the resources if caching is enabled + if (this.isCachingEnabled) { + this.resources = resources + } + + return resources } async getRootCategories(categoryType: ApiCategoryType): Promise { + // return cached hint root elements (if any) in order to not overload vRO if caching is enabled + if ( + this.isCachingEnabled && + this.rootElements?.has(categoryType) && + Array.isArray(this.rootElements?.get(categoryType)) + ) { + return this.childElements?.get(categoryType) as ApiElement[] + } + const responseJson: ContentLinksResponse = await this.send( "GET", `categories?isRoot=true&categoryType=${categoryType}` ) const categories = responseJson.link - .map(child => { - const name = child.attributes.find(att => att.name === "name") - const id = child.attributes.find(att => att.name === "id") + .map((item: LinkItem) => { + const name = item.attributes.find(att => att.name === "name") + const id = item.attributes.find(att => att.name === "id") return { - name: name ? name.value : undefined, - id: id ? id.value : undefined, + name: name?.value ?? undefined, + id: id?.value ?? undefined, type: categoryType, - rel: child.rel + rel: item?.rel, + description: item?.description?.value ?? undefined } }) - .filter(val => { - return val.name !== undefined && val.id !== undefined + .filter(item => { + return item.name !== undefined && item.id !== undefined }) as ApiElement[] - return categories.sort((x, y) => x.name.localeCompare(y.name)) + categories.sort((x, y) => x.name.localeCompare(y.name)) + + // cache the hint root elements in order not to overload vRO REST API if caching is enabled + if (this.isCachingEnabled) { + this.childElements.set(categoryType, categories) + } + + return categories } async getChildrenOfCategory(categoryId: string): Promise { + // return cached hint child elements (if any) in order to not overload vRO if caching is enabled + if ( + this.isCachingEnabled && + this.childElements?.has(categoryId) && + Array.isArray(this.childElements?.get(categoryId)) + ) { + return this.childElements?.get(categoryId) as ApiElement[] + } + const responseJson: ContentChildrenResponse = await this.send("GET", `categories/${categoryId}`) const children = responseJson.relations.link .map(child => { if (!child.attributes) { return undefined } - const name = child.attributes.find(att => att.name === "name") const id = child.attributes.find(att => att.name === "id") const type = child.attributes.find(att => att.name === "type") @@ -510,16 +592,31 @@ export class VroRestClient { return !!val && val.name !== undefined && val.id !== undefined && val.type !== undefined }) as ApiElement[] - return children.sort((x, y) => x.name.localeCompare(y.name)) + children.sort((x, y) => x.name.localeCompare(y.name)) + + // cache the hint child elements in order not to overload vRO REST API if caching is enabled + if (this.isCachingEnabled) { + this.childElements.set(categoryId, children) + } + + return children } async getChildrenOfCategoryWithDetails(categoryId: string): Promise { + // return cached hint actions (if any) in order to not overload vRO REST API if caching is enabled + if ( + this.isCachingEnabled && + this.hintActions?.has(categoryId) && + Array.isArray(this.hintActions?.get(categoryId)) + ) { + return this.hintActions?.get(categoryId) as HintAction[] + } + let responseJson: HintModule = { id: "", name: "", actions: [] } - try { responseJson = await this.send("GET", `server-configuration/api/category/${categoryId}`) } catch (error) { @@ -532,17 +629,22 @@ export class VroRestClient { const children: HintAction[] = responseJson.actions.map(child => { return { - name: child.name || undefined, - id: child.id || undefined, - returnType: child.returnType || undefined, - description: child.description || undefined, - version: child.version || undefined, - categoryId: child.categoryId || undefined, + name: child.name ?? undefined, + id: child.id ?? undefined, + returnType: child.returnType ?? undefined, + description: child.description ?? undefined, + version: child.version ?? undefined, + categoryId: child.categoryId ?? undefined, parameters: child.parameters || [] } as HintAction }) + children.sort((x, y) => x.name.localeCompare(y.name)) + // cache the hint actions in order not to overload vRO REST API if caching is enabled in config + if (this.isCachingEnabled) { + this.hintActions.set(categoryId, children) + } - return children.sort((x, y) => x.name.localeCompare(y.name)) + return children } async getResource(id: string): Promise { @@ -571,23 +673,20 @@ export class VroRestClient { } async getInventoryItems(href?: string): Promise { - const responseJson: ContentChildrenResponse = await this.send("GET", href || "inventory") + const responseJson: ContentChildrenResponse = await this.send("GET", href ?? "inventory") const children = responseJson.relations.link .map(child => { if (!child.attributes) { return undefined } - const id = - child.attributes.find(att => att.name === "id") || + child.attributes.find(att => att.name === "id") ?? child.attributes.find(att => att.name === "dunesId") - const name = - child.attributes.find(att => att.name === "displayName") || + child.attributes.find(att => att.name === "displayName") ?? child.attributes.find(att => att.name === "name") - const type = - child.attributes.find(att => att.name === "type") || + child.attributes.find(att => att.name === "type") ?? child.attributes.find(att => att.name === "@type") return { @@ -639,8 +738,8 @@ export class VroRestClient { uri: `https://${this.hostname}:${this.port}/vco/api/workflows/${id}/schema`, auth: { ...(await this.getAuth()) } } - const stream = request(options).pipe(fs.createWriteStream(targetPath)) + return new Promise((resolve, reject) => { stream.on("finish", () => resolve()) stream.on("error", e => reject(e)) @@ -727,4 +826,8 @@ export class VroRestClient { async getPluginDetails(link: string) { return this.send("GET", `server-configuration/api/plugins/${link}`) } + + private loadCacheConfiguration(): void { + this.isCachingEnabled = this.settings?.vrdev?.vro?.inventory?.cache ?? false + } } diff --git a/packages/node/vrdt-common/src/rest/vro-model.ts b/packages/node/vrdt-common/src/rest/vro-model.ts index 7e6e96c5..ab6d810e 100644 --- a/packages/node/vrdt-common/src/rest/vro-model.ts +++ b/packages/node/vrdt-common/src/rest/vro-model.ts @@ -47,27 +47,57 @@ export interface WorkflowLogsResponse { }[] } -export interface ApiElement { +export interface Resource { + id: string name: string +} + +// For better readability a separate interface is created +export interface Configuration extends Resource { + version: string +} + +// For better readability a separate interface is created +export interface Workflow extends Resource { + version: string +} + +export interface Action { id: string + fqn: string + version: string +} + +export interface ApiElement extends Resource { type: ApiCategoryType | ApiElementType rel: string } -export interface InventoryElement { +export interface InventoryElement extends Resource { + type: string + rel: string + href: string +} + +export interface BaseAttribute { name: string - id: string + value?: string +} + +export interface TypeAttribute extends BaseAttribute { type: string +} + +export interface LinkItem { + attributes: TypeAttribute[] rel: string href: string + description?: BaseAttribute } export interface ContentLinksResponse { - link: { - attributes: { name: string; value: string; type: string }[] - rel: string - href: string - }[] + link: LinkItem[] + total?: number } export interface ContentChildrenResponse { diff --git a/packages/node/vrdt-common/src/types/settings.ts b/packages/node/vrdt-common/src/types/settings.ts index 2113b429..a6d6e773 100644 --- a/packages/node/vrdt-common/src/types/settings.ts +++ b/packages/node/vrdt-common/src/types/settings.ts @@ -32,6 +32,15 @@ export interface TasksInfo { disable: boolean exclude: string[] } + +export interface VroInfo { + inventory: InventoryInfo +} + +export interface InventoryInfo { + cache: boolean +} + export interface BuildTools { defaultVersion: string } @@ -57,4 +66,5 @@ export interface VrealizeSettings { buildTools: BuildTools views: Views vra: VraInfo + vro: VroInfo } diff --git a/packages/node/vro-language-server/src/constants.ts b/packages/node/vro-language-server/src/constants.ts index 1cb5654c..cb98fdea 100644 --- a/packages/node/vro-language-server/src/constants.ts +++ b/packages/node/vro-language-server/src/constants.ts @@ -40,5 +40,16 @@ message Action { }` export const Timeout = { - ONE_SECOND: 1000 + ONE_SECOND: 1000, + THREE_SECONDS: 3000, + FIVE_SECONDS: 5000 +} + +export enum CompletionPrefixKind { + UNKNOWN = "Unknown", + CLASS_IMPORT = "Class Import", + CLASS_REFERENCE = "Class Reference", + STATIC_MEMBER_REFERENCE = "Static Member Reference", + NEW_INSTANCE = "New Instance", + MODULE_IMPORT = "Module Import" } diff --git a/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts b/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts index 9e0727cb..6da5d3eb 100644 --- a/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts +++ b/packages/node/vro-language-server/src/server/feature/CompletionProvider.ts @@ -17,15 +17,8 @@ import { URI } from "vscode-uri" import { ConnectionLocator, Environment, HintLookup } from "../core" import { Synchronizer, TextDocumentWrapper } from "../document" import { Previewer } from "../util" - -enum CompletionPrefixKind { - UNKNOWN = "Unknown", - CLASS_IMPORT = "Class Import", - CLASS_REFERENCE = "Class Reference", - STATIC_MEMBER_REFERENCE = "Static Member Reference", - NEW_INSTANCE = "New Instance", - MODULE_IMPORT = "Module Import" -} +import { CompletionPrefixKind } from "../../constants" +import { vmw } from "../../proto" interface CompletionPrefix { readonly value: string @@ -172,7 +165,7 @@ export class CompletionProvider { .concat(this.hints.getFunctionSets()) .filter(cls => !!cls.name && cls.name.startsWith(prefix.filter || "")) .map(cls => { - const name = cls.name || "" + const name = cls.name ?? "" const completionItem = CompletionItem.create(name) completionItem.kind = CompletionItemKind.Class completionItem.documentation = cls.description ? cls.description.trim() : undefined @@ -184,59 +177,6 @@ export class CompletionProvider { return suggestions } - private getStaticMemberSuggestions(prefix: CompletionPrefix): CompletionItem[] { - // TODO: Set isInstantiable back to `false`, once there are other ways to find out - // the members of non-static classes. At the moment, the only way to do that - // is to reference the class directly and check its members using the auto-complete feature - const cls = this.hints - .getClasses({ isInstantiable: undefined }) - .concat(this.hints.getFunctionSets()) - .find(c => c.name === prefix.value) - - if (!cls) return [] - - const suggestions: CompletionItem[] = [] - - if (cls.methods && cls.methods.length > 0) { - cls.methods - .filter(method => !!method.name && method.name.startsWith(prefix.filter || "")) - .forEach(method => { - const name = method.name || "" - const completionItem = CompletionItem.create(name) - completionItem.kind = CompletionItemKind.Method - completionItem.documentation = Previewer.extendDescriptionWithParams( - method.description, - method.parameters - ) - completionItem.detail = Previewer.computeDetailsForMethod(method) - completionItem.sortText = `000${name}` - suggestions.push(completionItem) - }) - } - - if (cls.properties && cls.properties.length > 0) { - cls.properties - .filter(prop => !!prop.name && prop.name.startsWith(prefix.filter || "")) - .forEach(prop => { - const name = prop.name || "" - const completionItem = CompletionItem.create(name) - completionItem.kind = CompletionItemKind.Variable - - if (prop.readOnly) { - completionItem.kind = - name.toUpperCase() === name ? CompletionItemKind.Enum : CompletionItemKind.Value - } - - completionItem.documentation = prop.description ? prop.description.trim() : undefined - completionItem.detail = Previewer.computeDetailsForProperty(prop) - completionItem.sortText = `000${name}` - suggestions.push(completionItem) - }) - } - - return suggestions - } - private getConstructorSuggestions(prefix: CompletionPrefix): CompletionItem[] { const suggestions: CompletionItem[] = [] @@ -246,7 +186,7 @@ export class CompletionProvider { .forEach(cls => { if (cls.constructors && cls.constructors.length > 0) { for (const constr of cls.constructors) { - const name = cls.name || "" + const name = cls.name ?? "" const completionItem = CompletionItem.create(name) completionItem.kind = CompletionItemKind.Constructor @@ -269,14 +209,90 @@ export class CompletionProvider { return suggestions } + private getStaticMemberSuggestions(prefix: CompletionPrefix): CompletionItem[] { + // TODO: Set isInstantiable back to `false`, once there are other ways to find out + // the members of non-static classes. At the moment, the only way to do that + // is to reference the class directly and check its members using the auto-complete feature + const cls = this.hints + .getClasses({ isInstantiable: undefined }) + .concat(this.hints.getFunctionSets()) + .find(c => c.name === prefix.value) + if (!cls) { + return [] + } + + const suggestions: CompletionItem[] = [] + const methods: CompletionItem[] = this.getMethodsSuggestions(cls, prefix) + const properties: CompletionItem[] = this.getPropertiesSuggestions(cls, prefix) + if (methods?.length) { + methods.forEach(item => suggestions.push(item)) + } + if (properties?.length) { + properties.forEach(item => suggestions.push(item)) + } + + return suggestions + } + + private getMethodsSuggestions(cls: vmw.pscoe.hints.IClass, prefix: CompletionPrefix): CompletionItem[] { + if (!cls.methods?.length) { + return [] + } + + const suggestions: CompletionItem[] = [] + cls.methods + .filter((method: vmw.pscoe.hints.IMethod) => !!method.name && method.name.startsWith(prefix.filter || "")) + .forEach((method: vmw.pscoe.hints.IMethod) => { + const name = method.name ?? "" + const completionItem = CompletionItem.create(name) + completionItem.kind = CompletionItemKind.Method + completionItem.documentation = Previewer.extendDescriptionWithParams( + method.description, + method.parameters + ) + completionItem.detail = Previewer.computeDetailsForMethod(method) + completionItem.sortText = `000${name}` + suggestions.push(completionItem) + }) + + return suggestions + } + + private getPropertiesSuggestions(cls: vmw.pscoe.hints.IClass, prefix: CompletionPrefix): CompletionItem[] { + if (!cls.properties?.length) { + return [] + } + + const suggestions: CompletionItem[] = [] + cls.properties + .filter((prop: vmw.pscoe.hints.IProperty) => !!prop.name && prop.name.startsWith(prefix.filter || "")) + .forEach((prop: vmw.pscoe.hints.IProperty) => { + const name = prop.name ?? "" + const completionItem = CompletionItem.create(name) + completionItem.kind = CompletionItemKind.Variable + + if (prop.readOnly) { + completionItem.kind = + name.toUpperCase() === name ? CompletionItemKind.Enum : CompletionItemKind.Value + } + + completionItem.documentation = prop.description ? prop.description.trim() : undefined + completionItem.detail = Previewer.computeDetailsForProperty(prop) + completionItem.sortText = `000${name}` + suggestions.push(completionItem) + }) + + return suggestions + } + private getModuleClassSuggestions(prefix: CompletionPrefix, workspaceFolder: WorkspaceFolder): CompletionItem[] { const suggestions = this.hints .getActionsIn(prefix.value, workspaceFolder) .filter(a => !!a.name && a.name.startsWith(prefix.filter || "")) .map(action => { - const name = action.name || "" + const name = action.name ?? "" const completionItem = CompletionItem.create(name) - const isClass = name[0] === name[0].toUpperCase() + const isClass = name[0].startsWith(name[0].toUpperCase()) completionItem.kind = isClass ? CompletionItemKind.Class : CompletionItemKind.Function completionItem.documentation = Previewer.extendDescriptionWithParams( action.description, @@ -294,7 +310,6 @@ export class CompletionProvider { const lineContent = document.getLineContentUntil(position) this.logger.debug(`Trying to provide auto completion for line '${lineContent}'`) - for (const pattern of prefixPatterns) { const prefix = pattern.match(lineContent) if (prefix) { @@ -302,8 +317,8 @@ export class CompletionProvider { return prefix } } - this.logger.debug("None of the patterns matched.") + return null } } diff --git a/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts b/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts index 6483be89..21304fc5 100644 --- a/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts +++ b/packages/node/vro-language-server/src/server/request/collection/ServerCollection.ts @@ -4,12 +4,13 @@ */ import { CancellationToken } from "vscode-languageserver" -import { AutoWire, HintAction, HintModule, HintPlugin, Logger, VroRestClient } from "@vmware/vrdt-common" +import { AutoWire, HintAction, HintModule, HintPlugin, Logger, sleep, VroRestClient } from "@vmware/vrdt-common" import { remote } from "../../../public" import { ConnectionLocator, Environment, HintLookup, Settings } from "../../core" import { WorkspaceCollection } from "./WorkspaceCollection" import { vmw } from "../../../proto" +import { Timeout } from "../../../constants" @AutoWire export class CollectionStatus { @@ -47,7 +48,7 @@ export class ServerCollection { connectionLocator.connection.onRequest(remote.server.triggerVroCollection, this.triggerCollection.bind(this)) - this.restClient = new VroRestClient(settings, environment) + this.restClient = new VroRestClient(settings) } giveCollectionStatus(): CollectionStatus { @@ -97,6 +98,7 @@ export class ServerCollection { async getModulesAndActions() { this.logger.info("Collecting Modules and Actions...") + // fetch the root script module categories const modules: HintModule[] = (await this.restClient.getRootCategories("ScriptModuleCategory")).map(module => { return { id: module.id, @@ -104,24 +106,18 @@ export class ServerCollection { actions: [] } }) - await Promise.all( - modules.map( - async module => - (module.actions = await this.restClient.getChildrenOfCategoryWithDetails(module.id).then(actions => - actions.map(action => { - return { - id: action.id, - name: action.name, - version: action.version, - description: action.description, - returnType: action.returnType, - parameters: action.parameters - } as HintAction - }) - )) - ) - ) + // add delay between the 2 REST calls in order not to overload the vRO vco service cache + // see also: https://kb.vmware.com/s/article/95783?lang=en_US + await this.setDelay(Timeout.THREE_SECONDS) + + // Enrichment of category actions execution has to be executed in serial order for not to overload the vRO + // see also: https://kb.vmware.com/s/article/95783?lang=en_US + for (const module of modules) { + await this.enrichHintModuleWithActions(module) + } + this.logger.info("Modules and Actions collected from vRO") + return modules } @@ -162,14 +158,12 @@ export class ServerCollection { this.logger.error("No vRO objects found") } else { for (const plugin of plugins) { - const link = plugin.detailsLink.match(regex) + const link = RegExp(regex).exec(plugin.detailsLink) if (!link) { throw new Error(`No plugin details found`) } - const parsedLink = link[0].substring(9).toString() // always retrieve and parse the first occurrence const pluginDetails = await this.restClient.getPluginDetails(parsedLink) - for (const pluginObject of pluginDetails["objects"]) { const object: vmw.pscoe.hints.IClass = { name: pluginObject["name"] @@ -196,4 +190,15 @@ export class ServerCollection { this.currentStatus.error = message this.currentStatus.finished = true } + + private async setDelay(delayMs: number) { + await sleep(delayMs) + } + + private async enrichHintModuleWithActions(module: HintModule): Promise { + const actions: HintAction[] = await this.restClient.getChildrenOfCategoryWithDetails(module.id) + module.actions = actions + + return module + } } diff --git a/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts b/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts index 87db92d7..64edabd6 100644 --- a/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts +++ b/packages/node/vro-language-server/src/server/request/collection/WorkspaceCollection.ts @@ -68,6 +68,7 @@ export class WorkspaceCollection { async triggerCollectionAndRefresh(workspaceFolder: WorkspaceFolder): Promise { await this.triggerCollection(workspaceFolder) + // workspace collection this.hints.refreshForWorkspace(workspaceFolder) } @@ -84,7 +85,6 @@ export class WorkspaceCollection { const fullPath = path.join(workspaceFolder.uri.fsPath, modules.join(",")) const modulesPath = path.join(fullPath, "src/main/resources") - try { const payload = this.collectLocalData(modulesPath) this.generateActionsPbFiles(payload, outputDir, workspaceFolder) diff --git a/wiki/Using-the-VS-Code-Extension.md b/wiki/Using-the-VS-Code-Extension.md index 71985ca4..af4afb9e 100644 --- a/wiki/Using-the-VS-Code-Extension.md +++ b/wiki/Using-the-VS-Code-Extension.md @@ -75,6 +75,10 @@ A vRO explorer view is available in the activity bar that allows browsing the wh ![vRO Explorer](./images/explorer.png) +#### Inventory Caching + +There is a support for vRO inventory items caching in order not to overload vRO on heavily loaded environments. If the vrdev:vro:inventory:cache setting is enabled in the plugin settings, the vRO inventory items will be fetched once from the vRO server during visual studion code session. When there are changes in vRO inventory made during visual studio code session they will not be fetched. In order to maintain fresh load of data either disable the cache setting or reload the visual studio code. + ### Push and Pull content The VS Code build tasks palette (Cmd+Shift+B / Ctrl+Shift+B) contains commands for pushing content to a live vRO/vRA instance and for pulling workflows, configurations, resources and vRA content back to your local machine – in a form suitable for committing into source control.