Skip to content

Commit ed381aa

Browse files
committed
Move existing functions to centralized history file
1 parent 4d56741 commit ed381aa

File tree

4 files changed

+296
-280
lines changed

4 files changed

+296
-280
lines changed

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import {
5858
import { getAuthType } from '../../../auth/utils'
5959
import fs from '../../../shared/fs/fs'
6060
import { setContext } from '../../../shared/vscode/setContext'
61-
import { readHistoryFile } from '../../../codewhisperer/service/transformByQ/transformationHubViewProvider'
61+
import { readHistoryFile } from '../../../codewhisperer/service/transformByQ/transformationHistoryHandler'
6262

6363
// These events can be interactions within the chat,
6464
// or elsewhere in the IDE

packages/core/src/codewhisperer/models/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ export const noChangesMadeMessage = "I didn't make any changes for this transfor
547547

548548
export const noOngoingJobMessage = 'No ongoing job.'
549549

550-
export const nothingToShowMessage = 'Nothing to show'
550+
export const noJobHistoryMessage = 'No job history'
551551

552552
export const jobStartedNotification =
553553
'Amazon Q is transforming your code. It can take 10 to 30 minutes to upgrade your code, depending on the size of your project. To monitor progress, go to the Transformation Hub.'
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import fs from '../../../shared/fs/fs'
8+
import path from 'path'
9+
import os from 'os'
10+
import * as CodeWhispererConstants from '../../models/constants'
11+
import { JDKVersion, TransformationType, transformByQState } from '../../models/model'
12+
import { getLogger } from '../../../shared/logger/logger'
13+
import { codeWhispererClient } from '../../../codewhisperer/client/codewhisperer'
14+
import { pollTransformationStatusUntilComplete } from '../../commands/startTransformByQ'
15+
import { downloadAndExtractResultArchive } from './transformApiHandler'
16+
import { ChatSessionManager } from '../../../amazonqGumby/chat/storages/chatSession'
17+
import { AuthUtil } from '../../util/authUtil'
18+
import { setMaven } from './transformFileHandler'
19+
import { convertToTimeString, isWithin30Days } from '../../../shared/datetime'
20+
21+
export async function readHistoryFile(): Promise<CodeWhispererConstants.HistoryObject[]> {
22+
const history: CodeWhispererConstants.HistoryObject[] = []
23+
const jobHistoryFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv')
24+
25+
if (!(await fs.existsFile(jobHistoryFilePath))) {
26+
return history
27+
}
28+
29+
const historyFile = await fs.readFileText(jobHistoryFilePath)
30+
const jobs = historyFile.split('\n')
31+
jobs.shift() // removes headers
32+
33+
// Process from end, stop at 10 valid entries
34+
for (let i = jobs.length - 1; i >= 0 && history.length < 10; i--) {
35+
const job = jobs[i]
36+
if (job && isWithin30Days(job.split('\t')[0])) {
37+
const jobInfo = job.split('\t')
38+
history.push({
39+
startTime: jobInfo[0],
40+
projectName: jobInfo[1],
41+
status: jobInfo[2],
42+
duration: jobInfo[3],
43+
diffPath: jobInfo[4],
44+
summaryPath: jobInfo[5],
45+
jobId: jobInfo[6],
46+
})
47+
}
48+
}
49+
return history
50+
}
51+
52+
/* Job refresh-related functions */
53+
54+
export async function refreshJob(jobId: string, currentStatus: string, projectName: string) {
55+
// fetch status from server
56+
let status = ''
57+
let duration = ''
58+
if (currentStatus === 'COMPLETED' || currentStatus === 'PARTIALLY_COMPLETED') {
59+
// job is already completed, no need to fetch status
60+
status = currentStatus
61+
} else {
62+
try {
63+
const response = await codeWhispererClient.codeModernizerGetCodeTransformation({
64+
transformationJobId: jobId,
65+
profileArn: undefined,
66+
})
67+
status = response.transformationJob.status ?? currentStatus
68+
if (response.transformationJob.endExecutionTime && response.transformationJob.creationTime) {
69+
duration = convertToTimeString(
70+
response.transformationJob.endExecutionTime.getTime() -
71+
response.transformationJob.creationTime.getTime()
72+
)
73+
}
74+
75+
getLogger().debug(
76+
'Code Transformation: Job refresh - Fetched status for job id: %s\n{Status: %s; Duration: %s}',
77+
jobId,
78+
status,
79+
duration
80+
)
81+
} catch (error) {
82+
getLogger().error(
83+
'Code Transformation: Error fetching status (job id: %s): %s',
84+
jobId,
85+
(error as Error).message
86+
)
87+
return
88+
}
89+
}
90+
91+
// retrieve artifacts and updated duration if available
92+
let jobHistoryPath: string = ''
93+
if (status === 'COMPLETED' || status === 'PARTIALLY_COMPLETED') {
94+
// artifacts should be available to download
95+
jobHistoryPath = await retrieveArtifacts(jobId, projectName)
96+
97+
// delete metadata and zipped code files, if they exist
98+
await fs.delete(path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'metadata.txt'), {
99+
force: true,
100+
})
101+
await fs.delete(path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'zipped-code.zip'), {
102+
force: true,
103+
})
104+
} else if (CodeWhispererConstants.validStatesForBuildSucceeded.includes(status)) {
105+
// still in progress on server side
106+
if (transformByQState.isRunning()) {
107+
getLogger().warn(
108+
'Code Transformation: There is a job currently running (id: %s). Cannot resume another job (id: %s)',
109+
transformByQState.getJobId(),
110+
jobId
111+
)
112+
return
113+
}
114+
transformByQState.setRefreshInProgress(true)
115+
const messenger = transformByQState.getChatMessenger()
116+
const tabID = ChatSessionManager.Instance.getSession().tabID
117+
messenger?.sendJobRefreshInProgressMessage(tabID!, jobId)
118+
await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history') // refreshing the table disables all jobs' refresh buttons while this one is resuming
119+
120+
// resume job and bring to completion
121+
try {
122+
status = await resumeJob(jobId, projectName, status)
123+
} catch (e: any) {
124+
getLogger().error('Code Transformation: Error resuming job (id: %s): %s', jobId, (e as Error).message)
125+
transformByQState.setJobDefaults()
126+
messenger?.sendJobFinishedMessage(tabID!, CodeWhispererConstants.refreshErrorChatMessage)
127+
void vscode.window.showErrorMessage(CodeWhispererConstants.refreshErrorNotification(jobId))
128+
await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history')
129+
return
130+
}
131+
132+
// download artifacts if available
133+
if (
134+
CodeWhispererConstants.validStatesForCheckingDownloadUrl.includes(status) &&
135+
!CodeWhispererConstants.failureStates.includes(status)
136+
) {
137+
duration = convertToTimeString(Date.now() - new Date(transformByQState.getStartTime()).getTime())
138+
jobHistoryPath = await retrieveArtifacts(jobId, projectName)
139+
}
140+
141+
// reset state
142+
transformByQState.setJobDefaults()
143+
messenger?.sendJobFinishedMessage(tabID!, CodeWhispererConstants.refreshCompletedChatMessage)
144+
} else {
145+
// FAILED or STOPPED job
146+
getLogger().info('Code Transformation: No artifacts available to download (job status = %s)', status)
147+
if (status === 'FAILED') {
148+
// if job failed on backend, mark it to disable the refresh button
149+
status = 'FAILED_BE' // this will be truncated to just 'FAILED' in the table
150+
}
151+
// delete metadata and zipped code files, if they exist
152+
await fs.delete(path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'metadata.txt'), {
153+
force: true,
154+
})
155+
await fs.delete(path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'zipped-code.zip'), {
156+
force: true,
157+
})
158+
}
159+
160+
if (status === currentStatus && !jobHistoryPath) {
161+
// no changes, no need to update file/table
162+
void vscode.window.showInformationMessage(CodeWhispererConstants.refreshNoUpdatesNotification(jobId))
163+
return
164+
}
165+
166+
void vscode.window.showInformationMessage(CodeWhispererConstants.refreshCompletedNotification(jobId))
167+
// update local file and history table
168+
169+
await updateHistoryFile(status, duration, jobHistoryPath, jobId)
170+
}
171+
172+
export async function retrieveArtifacts(jobId: string, projectName: string) {
173+
const resultsPath = path.join(os.homedir(), '.aws', 'transform', projectName, 'results') // temporary directory for extraction
174+
let jobHistoryPath = path.join(os.homedir(), '.aws', 'transform', projectName, jobId)
175+
176+
if (await fs.existsFile(path.join(jobHistoryPath, 'diff.patch'))) {
177+
getLogger().info('Code Transformation: Diff patch already exists for job id: %s', jobId)
178+
jobHistoryPath = ''
179+
} else {
180+
try {
181+
await downloadAndExtractResultArchive(jobId, resultsPath)
182+
183+
if (!(await fs.existsDir(path.join(jobHistoryPath, 'summary')))) {
184+
await fs.mkdir(path.join(jobHistoryPath, 'summary'))
185+
}
186+
await fs.copy(path.join(resultsPath, 'patch', 'diff.patch'), path.join(jobHistoryPath, 'diff.patch'))
187+
await fs.copy(
188+
path.join(resultsPath, 'summary', 'summary.md'),
189+
path.join(jobHistoryPath, 'summary', 'summary.md')
190+
)
191+
if (await fs.existsFile(path.join(resultsPath, 'summary', 'buildCommandOutput.log'))) {
192+
await fs.copy(
193+
path.join(resultsPath, 'summary', 'buildCommandOutput.log'),
194+
path.join(jobHistoryPath, 'summary', 'buildCommandOutput.log')
195+
)
196+
}
197+
} catch (error) {
198+
jobHistoryPath = ''
199+
} finally {
200+
// delete temporary extraction directory
201+
await fs.delete(resultsPath, { recursive: true, force: true })
202+
}
203+
}
204+
return jobHistoryPath
205+
}
206+
207+
export async function updateHistoryFile(status: string, duration: string, jobHistoryPath: string, jobId: string) {
208+
const history: string[][] = []
209+
const historyLogFilePath = path.join(os.homedir(), '.aws', 'transform', 'transformation_history.tsv')
210+
if (await fs.existsFile(historyLogFilePath)) {
211+
const historyFile = await fs.readFileText(historyLogFilePath)
212+
const jobs = historyFile.split('\n')
213+
jobs.shift() // removes headers
214+
if (jobs.length > 0) {
215+
for (const job of jobs) {
216+
if (job) {
217+
const jobInfo = job.split('\t')
218+
// startTime: jobInfo[0], projectName: jobInfo[1], status: jobInfo[2], duration: jobInfo[3], diffPath: jobInfo[4], summaryPath: jobInfo[5], jobId: jobInfo[6]
219+
if (jobInfo[6] === jobId) {
220+
// update any values if applicable
221+
jobInfo[2] = status
222+
if (duration) {
223+
jobInfo[3] = duration
224+
}
225+
if (jobHistoryPath) {
226+
jobInfo[4] = path.join(jobHistoryPath, 'diff.patch')
227+
jobInfo[5] = path.join(jobHistoryPath, 'summary', 'summary.md')
228+
}
229+
}
230+
history.push(jobInfo)
231+
}
232+
}
233+
}
234+
}
235+
236+
if (history.length === 0) {
237+
return
238+
}
239+
240+
// rewrite file
241+
await fs.writeFile(historyLogFilePath, 'date\tproject_name\tstatus\tduration\tdiff_patch\tsummary\tjob_id\n')
242+
const tsvContent = history.map((row) => row.join('\t')).join('\n') + '\n'
243+
await fs.appendFile(historyLogFilePath, tsvContent)
244+
245+
// update table content
246+
await vscode.commands.executeCommand('aws.amazonq.transformationHub.updateContent', 'job history', undefined, true)
247+
}
248+
249+
async function resumeJob(jobId: string, projectName: string, status: string) {
250+
// set state to prepare to resume job
251+
await setupTransformationState(jobId, projectName, status)
252+
// resume polling the job
253+
return await pollAndCompleteTransformation(jobId)
254+
}
255+
256+
async function setupTransformationState(jobId: string, projectName: string, status: string) {
257+
transformByQState.setJobId(jobId)
258+
transformByQState.setPolledJobStatus(status)
259+
transformByQState.setJobHistoryPath(path.join(os.homedir(), '.aws', 'transform', projectName, jobId))
260+
const metadataFile = await fs.readFileText(path.join(transformByQState.getJobHistoryPath(), 'metadata.txt'))
261+
const metadata = metadataFile.split('\t')
262+
transformByQState.setTransformationType(metadata[1] as TransformationType)
263+
transformByQState.setSourceJDKVersion(metadata[2] as JDKVersion)
264+
transformByQState.setTargetJDKVersion(metadata[3] as JDKVersion)
265+
transformByQState.setCustomDependencyVersionFilePath(metadata[4])
266+
transformByQState.setPayloadFilePath(
267+
path.join(os.homedir(), '.aws', 'transform', projectName, jobId, 'zipped-code.zip')
268+
)
269+
setMaven()
270+
transformByQState.setCustomBuildCommand(metadata[5])
271+
transformByQState.setTargetJavaHome(metadata[6])
272+
transformByQState.setProjectPath(metadata[7])
273+
transformByQState.setStartTime(metadata[8])
274+
}
275+
276+
async function pollAndCompleteTransformation(jobId: string) {
277+
const status = await pollTransformationStatusUntilComplete(
278+
jobId,
279+
AuthUtil.instance.regionProfileManager.activeRegionProfile
280+
)
281+
// delete payload and metadata files
282+
await fs.delete(transformByQState.getPayloadFilePath(), { force: true })
283+
await fs.delete(path.join(transformByQState.getJobHistoryPath(), 'metadata.txt'), { force: true })
284+
// delete temporary build logs file
285+
const logFilePath = path.join(os.tmpdir(), 'build-logs.txt')
286+
await fs.delete(logFilePath, { force: true })
287+
return status
288+
}

0 commit comments

Comments
 (0)