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

[WIP] - Amazon Q Code Transform: Enforce valid JDK for user provided JAVA_HOME #4945

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 35 additions & 8 deletions packages/core/src/amazonqGumby/chat/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { telemetry } from '../../../shared/telemetry/telemetry'
import { MetadataResult } from '../../../shared/telemetry/telemetryClient'
import { CodeTransformTelemetryState } from '../../telemetry/codeTransformTelemetryState'
import { getAuthType } from '../../../codewhisperer/service/transformByQ/transformApiHandler'
import { getJavaVersionStringUsedByMaven } from '../../../codewhisperer/service/transformByQ/transformMavenHandler'
import DependencyVersions from '../../models/dependencies'

// These events can be interactions within the chat,
Expand All @@ -65,7 +66,7 @@ export class GumbyController {
private readonly messenger: Messenger
private readonly sessionStorage: ChatSessionManager
private authController: AuthController

private readonly MaximumJavaHomeRetries = 3
public constructor(
private readonly chatControllerMessageListeners: ChatControllerEventEmitters,
messenger: Messenger,
Expand Down Expand Up @@ -294,13 +295,6 @@ export class GumbyController {
}

private async prepareProjectForSubmission(message: { pathToJavaHome: string; tabID: string }): Promise<void> {
if (message.pathToJavaHome) {
transformByQState.setJavaHome(message.pathToJavaHome)
getLogger().info(
`CodeTransformation: using JAVA_HOME = ${transformByQState.getJavaHome()} since source JDK does not match Maven JDK`
)
}

try {
this.sessionStorage.getSession().conversationState = ConversationState.COMPILING
this.messenger.sendCompilationInProgress(message.tabID)
Expand Down Expand Up @@ -380,6 +374,39 @@ export class GumbyController {
const pathToJavaHome = extractPath(data.message)

if (pathToJavaHome) {
transformByQState.setJavaHome(pathToJavaHome)
getLogger().info(
`CodeTransformation: using JAVA_HOME = ${transformByQState.getJavaHome()} since source JDK does not match Maven JDK`
)

try {
await validateCanCompileProject()
} catch (err: any) {
if (err instanceof JavaHomeNotSetError) {
const providedJdkVersion =
(await getJavaVersionStringUsedByMaven()) ?? JDKVersion.UNSUPPORTED
const expectedJdkVersion = transformByQState.getSourceJDKVersion() ?? JDKVersion.UNSUPPORTED
getLogger().warn(
`CodeTransformation: non matching JAVA_HOME provided: ${providedJdkVersion} expected: ${expectedJdkVersion} JDK release must match`
)
if (transformByQState.incrementAndGetJavaHomeAttempts() > this.MaximumJavaHomeRetries) {
transformByQState.resetJavaHomeAttempts()
transformByQState.resetJavaHome()
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
return
}
this.sessionStorage.getSession().conversationState = ConversationState.PROMPT_JAVA_HOME
this.messenger.sendInvalidJavaHomeProvidedMessage(data.tabID, expectedJdkVersion)
this.messenger.sendUpdatePlaceholder(
data.tabID,
MessengerUtils.createInvalidJavaHomePlaceholder(expectedJdkVersion)
)
this.messenger.sendChatInputEnabled(data.tabID, true)
return
}
throw err
}

await this.prepareProjectForSubmission({
pathToJavaHome,
tabID: data.tabID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type StaticTextResponseType =
| 'java-home-not-set'
| 'start-transformation-confirmed'
| 'job-transmitted'
| 'invalid-java-home-provided'
| 'end-HIL-early'

export type UnrecoverableErrorType =
Expand Down Expand Up @@ -198,9 +199,22 @@ export class Messenger {
this.dispatcher.sendAsyncEventProgress(new AsyncEventProgressMessage(tabID, { inProgress, message, messageId }))
}

public sendInvalidJavaHomeProvidedMessage(tabID: string, expectedJdkVersion: string) {
this.dispatcher.sendChatMessage(
new ChatMessage(
{
message: MessengerUtils.createInvalidJavaHomePromptChatMessage(expectedJdkVersion),
messageType: 'ai-prompt',
},
tabID
)
)
}

public sendCompilationInProgress(tabID: string) {
const message = CodeWhispererConstants.buildStartedChatMessage

// Mynah UI requires us sending `message: undefined` before passing the `message` for animation to work
this.dispatcher.sendAsyncEventProgress(
new AsyncEventProgressMessage(tabID, { inProgress: true, message: undefined })
)
Expand Down Expand Up @@ -401,6 +415,7 @@ export class Messenger {
}

public sendTransformationIntroduction(tabID: string) {
// Mynah UI requires us sending `message: undefined` before passing the `message` for animation to work
this.dispatcher.sendAsyncEventProgress(
new AsyncEventProgressMessage(tabID, { inProgress: true, message: undefined })
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,36 @@ export enum GumbyCommands {
}

export default class MessengerUtils {
static createJavaHomePrompt = (): string => {
let javaHomePrompt = `${
CodeWhispererConstants.enterJavaHomeChatMessage
} ${transformByQState.getSourceJDKVersion()}. \n`
static createInstructionsForFindingJavaHome = () => {
const jdkVersion = transformByQState.getSourceJDKVersion()
if (os.platform() === 'win32') {
javaHomePrompt += CodeWhispererConstants.windowsJavaHomeHelpChatMessage.replace(
'JAVA_VERSION_HERE',
transformByQState.getSourceJDKVersion()!
)
return CodeWhispererConstants.windowsJavaHomeHelpChatMessage.replace('JAVA_VERSION_HERE', jdkVersion!)
} else {
const jdkVersion = transformByQState.getSourceJDKVersion()
if (jdkVersion === JDKVersion.JDK8) {
javaHomePrompt += ` ${CodeWhispererConstants.nonWindowsJava8HomeHelpChatMessage}`
return ` ${CodeWhispererConstants.nonWindowsJava8HomeHelpChatMessage}`
} else if (jdkVersion === JDKVersion.JDK11) {
javaHomePrompt += ` ${CodeWhispererConstants.nonWindowsJava11HomeHelpChatMessage}`
return ` ${CodeWhispererConstants.nonWindowsJava11HomeHelpChatMessage}`
}
}
return javaHomePrompt
return ''
}

static createJavaHomePrompt = (): string => {
return (
`${CodeWhispererConstants.enterJavaHomeChatMessage} ${transformByQState.getSourceJDKVersion()}.\n` +
this.createInstructionsForFindingJavaHome()
)
}

static createInvalidJavaHomePromptChatMessage = (expectedJdkVersion: string): string =>
CodeWhispererConstants.invalidJavaHomeProvidedChatMessage(
expectedJdkVersion,
this.createInstructionsForFindingJavaHome()
)

static createInvalidJavaHomePlaceholder = (expectedJdkVersion: string): string =>
CodeWhispererConstants.invalidJavaHomeProvidedPlaceholder(expectedJdkVersion)

static stringToEnumValue = <T extends { [key: string]: string }, K extends keyof T & string>(
enumObject: T,
value: `${T[K]}`
Expand Down
16 changes: 4 additions & 12 deletions packages/core/src/codewhisperer/commands/startTransformByQ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { getOpenProjects, validateOpenProjects } from '../service/transformByQ/t
import {
getVersionData,
prepareProjectDependencies,
getJavaVersionUsedByMaven,
runMavenDependencyUpdateCommands,
} from '../service/transformByQ/transformMavenHandler'
import {
Expand Down Expand Up @@ -104,16 +105,7 @@ async function setMaven() {
}

async function validateJavaHome(): Promise<boolean> {
const versionData = await getVersionData()
let javaVersionUsedByMaven = versionData[1]
if (javaVersionUsedByMaven !== undefined) {
javaVersionUsedByMaven = javaVersionUsedByMaven.slice(0, 3)
if (javaVersionUsedByMaven === '1.8') {
javaVersionUsedByMaven = JDKVersion.JDK8
} else if (javaVersionUsedByMaven === '11.') {
javaVersionUsedByMaven = JDKVersion.JDK11
}
}
const javaVersionUsedByMaven = await getJavaVersionUsedByMaven()
if (javaVersionUsedByMaven !== transformByQState.getSourceJDKVersion()) {
telemetry.codeTransform_isDoubleClickedToTriggerInvalidProject.emit({
codeTransformSessionId: CodeTransformTelemetryState.instance.getSessionId(),
Expand Down Expand Up @@ -637,8 +629,8 @@ export async function postTransformationJob() {
const resultStatusMessage = transformByQState.getStatus()

const versionInfo = await getVersionData()
const mavenVersionInfoMessage = `${versionInfo[0]} (${transformByQState.getMavenName()})`
const javaVersionInfoMessage = `${versionInfo[1]} (${transformByQState.getMavenName()})`
const mavenVersionInfoMessage = `${versionInfo.mavenVersion} (${transformByQState.getMavenName()})`
const javaVersionInfoMessage = `${versionInfo.javaVersion} (${transformByQState.getMavenName()})`

// Note: IntelliJ implementation of ResultStatusMessage includes additional metadata such as jobId.
telemetry.codeTransform_totalRunTime.emit({
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/codewhisperer/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,12 @@ export const cleanInstallErrorNotification =

export const enterJavaHomeChatMessage = 'Enter the path to JDK '

export const invalidJavaHomeProvidedChatMessage = (expectedJdkVersion: string, instructions: string) =>
`It looks like the path you provided is not where JDK ${expectedJdkVersion} is installed. Since you're transforming Java ${expectedJdkVersion} code, I need the JDK ${expectedJdkVersion} path. To find the JDK ${expectedJdkVersion} path, run the following command in a new IDE terminal:\n ${instructions}`

export const invalidJavaHomeProvidedPlaceholder = (expectedJdkVersion: string) =>
`Enter the JDK ${expectedJdkVersion} path`

export const projectPromptChatMessage =
'I can upgrade your JAVA_VERSION_HERE. To start the transformation, I need some information from you. Choose the project you want to upgrade and the target code version to upgrade to. Then, choose Transform.'

Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/codewhisperer/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ export class TransformByQState {

private intervalId: NodeJS.Timeout | undefined = undefined

private numAttemptsToGetJavaHome = 0

public isNotStarted() {
return this.transformByQState === TransformByQStatus.NotStarted
}
Expand Down Expand Up @@ -621,6 +623,18 @@ export class TransformByQState {
this.javaHome = javaHome
}

public resetJavaHome() {
this.javaHome = undefined
}

public incrementAndGetJavaHomeAttempts() {
return ++this.numAttemptsToGetJavaHome
}

public resetJavaHomeAttempts() {
this.numAttemptsToGetJavaHome = 0
}

public setChatControllers(controllers: ChatControllerEventEmitters) {
this.chatControllers = controllers
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'
import { FolderInfo, transformByQState } from '../../models/model'
import { FolderInfo, transformByQState, JDKVersion } from '../../models/model'
import { getLogger } from '../../../shared/logger'
import * as CodeWhispererConstants from '../../models/constants'
import { spawnSync } from 'child_process' // Consider using ChildProcess once we finalize all spawnSync calls
import { spawnSync, SpawnSyncOptionsWithStringEncoding } from 'child_process' // Consider using ChildProcess once we finalize all spawnSync calls
import { CodeTransformMavenBuildCommand, telemetry } from '../../../shared/telemetry/telemetry'
import { CodeTransformTelemetryState } from '../../../amazonqGumby/telemetry/codeTransformTelemetryState'
import { MetadataResult } from '../../../shared/telemetry/telemetryClient'
import { ToolkitError } from '../../../shared/errors'
import { writeLogs } from './transformFileHandler'
import { throwIfCancelled } from './transformApiHandler'

interface MavenVersionData {
mavenVersion: string | undefined
javaVersion: string | undefined
}

// run 'install' with either 'mvnw.cmd', './mvnw', or 'mvn' (if wrapper exists, we use that, otherwise we use regular 'mvn')
function installProjectDependencies(dependenciesFolder: FolderInfo, modulePath: string) {
// baseCommand will be one of: '.\mvnw.cmd', './mvnw', 'mvn'
Expand Down Expand Up @@ -174,11 +179,37 @@ export async function prepareProjectDependencies(dependenciesFolder: FolderInfo,
void vscode.window.showInformationMessage(CodeWhispererConstants.buildSucceededNotification)
}

export async function getVersionData() {
export async function getJavaVersionStringUsedByMaven() {
const versionData = await getVersionData()
return versionData.javaVersion?.slice(0, 3)
}

export async function getJavaVersionUsedByMaven() {
const javaVersion = await getJavaVersionStringUsedByMaven()
switch (javaVersion) {
case '1.8':
return JDKVersion.JDK8
case '11.':
return JDKVersion.JDK11
default:
return JDKVersion.UNSUPPORTED
}
}

export async function getVersionData(): Promise<MavenVersionData> {
const baseCommand = transformByQState.getMavenName() // will be one of: 'mvnw.cmd', './mvnw', 'mvn'
const modulePath = transformByQState.getProjectPath()
const javaHome = transformByQState.getJavaHome() // If customer provided JAVA_HOME use that

const args = ['-v']
const spawnResult = spawnSync(baseCommand, args, { cwd: modulePath, shell: true, encoding: 'utf-8' })
let env = process.env
if (javaHome) {
getLogger().info(`CodeTransformation: using customer provided JAVA_HOME = ${javaHome}`)
env = { ...env, JAVA_HOME: javaHome }
}

const options: SpawnSyncOptionsWithStringEncoding = { cwd: modulePath, shell: true, encoding: 'utf-8', env: env }
const spawnResult = spawnSync(baseCommand, args, options)

let localMavenVersion: string | undefined = ''
let localJavaVersion: string | undefined = ''
Expand All @@ -202,7 +233,7 @@ export async function getVersionData() {
getLogger().info(
`CodeTransformation: Ran ${baseCommand} to get Maven version = ${localMavenVersion} and Java version = ${localJavaVersion} with project JDK = ${transformByQState.getSourceJDKVersion()}`
)
return [localMavenVersion, localJavaVersion]
return { mavenVersion: localMavenVersion, javaVersion: localJavaVersion }
}

// run maven 'versions:dependency-updates-aggregate-report' with either 'mvnw.cmd', './mvnw', or 'mvn' (if wrapper exists, we use that, otherwise we use regular 'mvn')
Expand Down