From 62fe34c83f97d126cbe1d7e248abf540c266b3f0 Mon Sep 17 00:00:00 2001 From: Nikolay Blagoev Date: Sat, 25 Jan 2020 11:51:34 +0200 Subject: [PATCH] Fixes #48 - Support 'Run Action' against vRO 7.6 and vRO 8 Signed-off-by: Nikolay Blagoev --- common/src/rest/VroRestClient.ts | 39 ++- common/src/rest/api.ts | 1 + extension/src/client/command/RunAction.ts | 289 ++++++++++++++++------ 3 files changed, 248 insertions(+), 81 deletions(-) diff --git a/common/src/rest/VroRestClient.ts b/common/src/rest/VroRestClient.ts index 195276f8..c58dad41 100644 --- a/common/src/rest/VroRestClient.ts +++ b/common/src/rest/VroRestClient.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2018-2019 VMware, Inc. + * Copyright 2018-2020 VMware, Inc. * SPDX-License-Identifier: MIT */ @@ -156,7 +156,7 @@ export class VroRestClient { return execToken } - async getWorkflowLogs( + async getWorkflowLogsPre76( workflowId: string, executionId: string, severity: string, @@ -199,6 +199,41 @@ export class VroRestClient { return messages } + async getWorkflowLogsPost76( + workflowId: string, + executionId: string, + severity: string, + timestamp: number + ): Promise { + const response: WorkflowLogsResponse = await this.send( + "GET", + `workflows/${workflowId}/executions/${executionId}/syslogs` + + `?conditions=severity=${severity}` + + `&conditions=timestamp${encodeURIComponent(">")}${timestamp}` + + "&conditions=type=system" + ) + + 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, + description + }) + } + + return messages + } + async importPackage(path: string): Promise { return this.send("POST", "content/packages?overwrite=true", { formData: { diff --git a/common/src/rest/api.ts b/common/src/rest/api.ts index a3cacf5c..a787de86 100644 --- a/common/src/rest/api.ts +++ b/common/src/rest/api.ts @@ -42,6 +42,7 @@ export interface WorkflowLogsResponse { "time-stamp": string "short-description": string "long-description": string + "time-stamp-val": number } }[] } diff --git a/extension/src/client/command/RunAction.ts b/extension/src/client/command/RunAction.ts index 98b08ee8..fc5d6e62 100644 --- a/extension/src/client/command/RunAction.ts +++ b/extension/src/client/command/RunAction.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2018-2019 VMware, Inc. + * Copyright 2018-2020 VMware, Inc. * SPDX-License-Identifier: MIT */ @@ -9,7 +9,16 @@ import * as fs from "fs-extra" import * as moment from "moment" import * as semver from "semver" import * as tmp from "tmp" -import { AutoWire, Logger, MavenCliProxy, PomFile, sleep, VroRestClient, VrotscCliProxy } from "vrealize-common" +import { + AutoWire, + Logger, + LogMessage, + MavenCliProxy, + PomFile, + sleep, + VroRestClient, + VrotscCliProxy +} from "vrealize-common" import * as vscode from "vscode" import { Commands, OutputChannels } from "../constants" @@ -24,8 +33,6 @@ const RUN_SCRIPT_WORKFLOW_ID = "98568979-76ed-4a4a-854b-1e730e2ef4f1" @AutoWire export class RunAction extends Command { private readonly logger = Logger.get("RunAction") - private readonly restClient: VroRestClient - private readonly mavenProxy: MavenCliProxy private readonly vrotsc: VrotscCliProxy private readonly outputChannel = vscode.window.createOutputChannel(OutputChannels.RunActionLogs) private runtimeExceptionDecoration: vscode.TextEditorDecorationType @@ -38,8 +45,6 @@ export class RunAction extends Command { constructor(private config: ConfigurationManager, private environment: EnvironmentManager) { super() - this.restClient = new VroRestClient(config, environment) - this.mavenProxy = new MavenCliProxy(environment, config.vrdev.maven, this.logger) this.vrotsc = new VrotscCliProxy(this.logger) } @@ -91,22 +96,59 @@ export class RunAction extends Command { const scriptContent = await this.getScriptContent(activeTextEditor.document) this.outputChannel.appendLine(`# Running ${activeFileName}`) - const supportsSysLog = await this.supportsSysLog() - const token = await this.runScript(context, scriptContent, supportsSysLog) - this.outputChannel.appendLine(`# Execution ID: ${token}\n`) - const finalState = await this.waitToFinish(token, activeTextEditor, supportsSysLog) - const duration = await this.calculateExecutionTime(token) - const finished = finalState.charAt(0).toUpperCase() + finalState.slice(1) - this.outputChannel.appendLine(`\n# ${finished} after ${duration}`) + const actionRunner = new ActionRunner(this.config, this.environment) + await actionRunner.prepare(context) + + await actionRunner.run(scriptContent, (version, token) => { + this.outputChannel.appendLine(`# vRO Version: ${version}`) + this.outputChannel.appendLine(`# Execution ID: ${token}\n`) + }) + + await actionRunner.observe( + (message: string) => this.onLog(message), + (lineNumber: number, message: string) => this.onError(lineNumber, message, activeTextEditor), + (state: string, duration: string) => this.onEnd(state, duration) + ) } catch (e) { const errorMessage = typeof e === "string" ? e : e.message this.outputChannel.appendLine(`# An error occurred: ${errorMessage}`) + this.logger.error(e) + this.logger.debug(e.stack) vscode.window.showErrorMessage(errorMessage) } finally { this.running = false } } + private onLog(message: string) { + this.outputChannel.appendLine(message) + } + + private onError(lineNumber: number, message: string, editor: vscode.TextEditor) { + const hasIife = IIFE_WRAPPER_PATTERN.exec(editor.document.getText()) + + if (hasIife) { + const offsetIndex = hasIife.index // store the position of the beggining of the regex match + if (offsetIndex > 0) { + lineNumber += editor.document.positionAt(offsetIndex).line + } + } + + const range = editor.document.lineAt(lineNumber - 1).range + const filename = path.basename(editor.document.uri.fsPath) + const hoverMessage = [ + new vscode.MarkdownString(`A runtime exception ocurred while executing script _${filename}_`), + new vscode.MarkdownString(`\`${message}\``) + ] + const decoration = { range, hoverMessage } + editor.setDecorations(this.runtimeExceptionDecoration, [decoration]) + } + + private onEnd(state: string, duration: string) { + const capitalizedState = state.charAt(0).toUpperCase() + state.slice(1) + this.outputChannel.appendLine(`\n# ${capitalizedState} after ${duration}`) + } + private validateFileType(document: vscode.TextDocument): boolean { if (document.languageId !== "javascript" && document.languageId !== "typescript") { vscode.window.showErrorMessage("The currently opened file is not a TypeScript or JavaScript file") @@ -173,12 +215,21 @@ export class RunAction extends Command { return Promise.reject(`Unsupported language ID: ${document.languageId}`) } +} + +class ActionRunner { + private readonly logger = Logger.get("ActionRunner") + private readonly restClient: VroRestClient + private readonly mavenProxy: MavenCliProxy + private vroVersion: string + private executionToken: string + + constructor(config: ConfigurationManager, private environment: EnvironmentManager) { + this.restClient = new VroRestClient(config, environment) + this.mavenProxy = new MavenCliProxy(environment, config.vrdev.maven, this.logger) + } - private async runScript( - context: vscode.ExtensionContext, - scriptContent: string, - supportsSysLog: boolean - ): Promise { + async prepare(context: vscode.ExtensionContext) { try { this.logger.info(`Checking if workflow with ID ${RUN_SCRIPT_WORKFLOW_ID} exists in target vRO`) await this.restClient.getWorkflow(RUN_SCRIPT_WORKFLOW_ID) @@ -206,6 +257,10 @@ export class RunAction extends Command { } ) } + } + + async run(scriptContent: string, callback: (version: string, token: string) => void): Promise { + this.vroVersion = await this.getVroVersion() let fileContent = scriptContent const hasIife = IIFE_WRAPPER_PATTERN.exec(fileContent) @@ -213,7 +268,8 @@ export class RunAction extends Command { fileContent = hasIife[1] } - this.logger.info(`Running workflow ${RUN_SCRIPT_WORKFLOW_ID}`) + this.logger.info(`Running workflow ${RUN_SCRIPT_WORKFLOW_ID} (vRO ${this.vroVersion})`) + const supportsSysLog = semver.gt(this.vroVersion, "7.3.1") const params = [ { name: "script", @@ -231,12 +287,12 @@ export class RunAction extends Command { } ] - const token = await this.restClient.startWorkflow(RUN_SCRIPT_WORKFLOW_ID, ...params) - return token + this.executionToken = await this.restClient.startWorkflow(RUN_SCRIPT_WORKFLOW_ID, ...params) + callback(this.vroVersion, this.executionToken) } private async getExecPackage(context: vscode.ExtensionContext): Promise { - const storagePath = context["globalStoragePath"] + const storagePath = context.globalStoragePath if (!fs.existsSync(storagePath)) { fs.mkdirSync(storagePath) } @@ -251,90 +307,165 @@ export class RunAction extends Command { return path.join(storagePath, "exec.package") } - private async waitToFinish(token: string, editor: vscode.TextEditor, supportsSysLog: boolean): Promise { - const printedMessages = new Set() - let lastLogTimestamp = 0 + async observe( + onLog: (message: string) => void, + onError: (lineNumber: number, message: string) => void, + onEnd: (state: string, duration: string) => void + ): Promise { + const fetchLogsStrategy = this.getLoggingStrategy(onLog, onError) + const finalStates = ["completed", "failed", "canceled"] let state = "initializing" do { - if (supportsSysLog) { - lastLogTimestamp = await this.printMessagesSince(token, printedMessages, lastLogTimestamp, editor) - } + await fetchLogsStrategy.printMessages() await sleep(200) - state = await this.restClient.getWorkflowExecutionState(RUN_SCRIPT_WORKFLOW_ID, token) + state = await this.restClient.getWorkflowExecutionState(RUN_SCRIPT_WORKFLOW_ID, this.executionToken) this.logger.debug(`Workflow ${RUN_SCRIPT_WORKFLOW_ID} execution state: ${state}`) - } while (["completed", "failed", "canceled"].indexOf(state) < 0) - - if (supportsSysLog) { - await sleep(1500) - await this.printMessagesSince(token, printedMessages, lastLogTimestamp, editor) - } else { - const execution = await this.restClient.getWorkflowExecution(RUN_SCRIPT_WORKFLOW_ID, token) - const logs = execution["output-parameters"][0].value.string.value - this.outputChannel.appendLine(logs) + } while (finalStates.indexOf(state) < 0) + + await fetchLogsStrategy.finalize() + onEnd(state, await this.getDuration()) + } + + private getLoggingStrategy( + onLog: (message: string) => void, + onError: (lineNumber: number, message: string) => void + ): FetchLogsStrategy { + if (semver.lt(this.vroVersion, "7.4.0")) { + return new FetchLogsPre74(onLog, onError, this.executionToken, this.restClient) + } else if (semver.lt(this.vroVersion, "7.6.0")) { + return new FetchLogsPre76(onLog, onError, this.executionToken, this.restClient) } - return state + return new FetchLogsPost76(onLog, onError, this.executionToken, this.restClient) + } + + private async getDuration(): Promise { + const execution = await this.restClient.getWorkflowExecution(RUN_SCRIPT_WORKFLOW_ID, this.executionToken) + const start = moment(execution["start-date"]) + const end = moment(execution["end-date"]) + const duration = moment.duration(end.diff(start)) + + return moment.utc(duration.as("milliseconds")).format("m[m] s[s]") + } + + private async getVroVersion(): Promise { + const versionInfo = await this.restClient.getVersion() + return versionInfo.version.replace(`.${versionInfo["build-number"]}`, "") + } +} + +abstract class FetchLogsStrategy { + constructor( + protected readonly log: (message: string) => void, + protected readonly error: (lineNumber: number, message: string) => void, + protected readonly executionToken: string, + protected readonly restClient: VroRestClient + ) {} + + abstract async printMessages(): Promise + abstract async finalize(): Promise +} + +class FetchLogsPre74 extends FetchLogsStrategy { + constructor( + onLog: (message: string) => void, + onError: (lineNumber: number, message: string) => void, + executionToken: string, + restClient: VroRestClient + ) { + super(onLog, onError, executionToken, restClient) + } + + async printMessages(): Promise { + // we do nothing since vRO API below version 7.4 + // does not support getting the execution logs + } + + async finalize(): Promise { + const execution = await this.restClient.getWorkflowExecution(RUN_SCRIPT_WORKFLOW_ID, this.executionToken) + const logs = execution["output-parameters"][0].value.string.value + this.log(logs) + } +} + +abstract class FetchSysLogsStrategy extends FetchLogsStrategy { + protected readonly printedMessages = new Set() + protected lastTimestamp = 0 + + constructor( + onLog: (message: string) => void, + onError: (lineNumber: number, message: string) => void, + executionToken: string, + restClient: VroRestClient + ) { + super(onLog, onError, executionToken, restClient) } - private async printMessagesSince( - token: string, - printedMessages: Set, - lastLogTimestamp: number, - editor: vscode.TextEditor - ): Promise { + protected abstract getLogMessages(): Promise + + async printMessages(): Promise { const timestamp = Date.now() - 10000 // 10sec earlier - const logs = await this.restClient.getWorkflowLogs(RUN_SCRIPT_WORKFLOW_ID, token, "debug", lastLogTimestamp) + const logs = await this.getLogMessages() logs.forEach(logMessage => { const timestamp = moment(logMessage.timestamp).format("YYYY-MM-DD HH:mm:ss.SSS ZZ") const msg = `[${timestamp}] [${logMessage.severity}] ${logMessage.description}` - if (!printedMessages.has(msg)) { - this.outputChannel.appendLine(msg) - printedMessages.add(msg) + if (!this.printedMessages.has(msg)) { + this.log(msg) + this.printedMessages.add(msg) const hasErrorLineNumber = SCRIPT_ERROR_LINE_PATTERN.exec(msg) if (hasErrorLineNumber) { - this.highlightError(parseInt(hasErrorLineNumber[1], 10), hasErrorLineNumber[2], editor) + this.error(parseInt(hasErrorLineNumber[1], 10), hasErrorLineNumber[2]) } } }) - return timestamp + this.lastTimestamp = timestamp } - private async calculateExecutionTime(token: string): Promise { - const execution = await this.restClient.getWorkflowExecution(RUN_SCRIPT_WORKFLOW_ID, token) - const start = moment(execution["start-date"]) - const end = moment(execution["end-date"]) - const duration = moment.duration(end.diff(start)) - - return moment.utc(duration.as("milliseconds")).format("m[m] s[s]") + async finalize(): Promise { + await sleep(1500) // give some time to vRO to flush the logs on disk + await this.printMessages() } +} - private highlightError(lineNumber: number, message: string, editor: vscode.TextEditor) { - const hasIife = IIFE_WRAPPER_PATTERN.exec(editor.document.getText()) - - if (hasIife) { - const offsetIndex = hasIife.index // store the position of the beggining of the regex match - if (offsetIndex > 0) { - lineNumber += editor.document.positionAt(offsetIndex).line - } - } +class FetchLogsPre76 extends FetchSysLogsStrategy { + constructor( + onLog: (message: string) => void, + onError: (lineNumber: number, message: string) => void, + executionToken: string, + restClient: VroRestClient + ) { + super(onLog, onError, executionToken, restClient) + } - const range = editor.document.lineAt(lineNumber - 1).range - const filename = path.basename(editor.document.uri.fsPath) - const hoverMessage = [ - new vscode.MarkdownString(`A runtime exception ocurred while executing script _${filename}_`), - new vscode.MarkdownString(`\`${message}\``) - ] - const decoration = { range, hoverMessage } - editor.setDecorations(this.runtimeExceptionDecoration, [decoration]) + protected getLogMessages(): Promise { + return this.restClient.getWorkflowLogsPre76( + RUN_SCRIPT_WORKFLOW_ID, + this.executionToken, + "debug", + this.lastTimestamp + ) } +} - private async supportsSysLog(): Promise { - const versionInfo = await this.restClient.getVersion() - const version = versionInfo.version.replace(`.${versionInfo["build-number"]}`, "") +class FetchLogsPost76 extends FetchSysLogsStrategy { + constructor( + onLog: (message: string) => void, + onError: (lineNumber: number, message: string) => void, + executionToken: string, + restClient: VroRestClient + ) { + super(onLog, onError, executionToken, restClient) + } - return semver.gt(version, "7.3.1") + protected getLogMessages(): Promise { + return this.restClient.getWorkflowLogsPost76( + RUN_SCRIPT_WORKFLOW_ID, + this.executionToken, + "debug", + this.lastTimestamp + ) } }