import { CancellationToken , window } from 'vscode' ; import { IActionContext , IParsedError , parseError } from 'vscode-azureextensionui' ; import { KuduClient , KuduModels } from 'vscode-azurekudu' ; import { createKuduClient } from '../createKuduClient' ; import { ext } from '../extensionVariables' ; import { localize } from '../localize' ; import { ParsedSite } from '../SiteClient' ; import { delay } from '../utils/delay' ; import { ignore404Error , retryKuduCall } from '../utils/kuduUtils' ; import { nonNullProp } from '../utils/nonNull' ; import { IDeployContext } from './IDeployContext' ; export async function waitForDeploymentToComplete ( context , site , expectedId ? , token ? , pollingInterval = 5000 ) { let fullLog = '' ; let lastLogTime = new Date ( 0 ) ; let lastErrorLine = '' ; let initialStartTime ; let deployment ; let permanentId ; const maxTimeToWaitForExpectedId = Date . now ( ) + 60 * 1000 ; const kuduClient = await createKuduClient ( context , site ) ; while ( ! token ?. isCancellationRequested ) { [ deployment , permanentId , initialStartTime ] = await tryGetLatestDeployment ( context , kuduClient , permanentId , initialStartTime , expectedId ) ; if ( ( deployment === undefined || ! deployment . id ) ) { if ( expectedId && Date . now ( ) < maxTimeToWaitForExpectedId ) { await delay ( pollingInterval ) ; continue ; } throw new Error ( localize ( 'failedToFindDeployment' , 'Failed to get status of deployment.' ) ) ; } const deploymentId = deployment . id ; let logEntries = [ ] ; await retryKuduCall ( context , 'getLogEntry' , async ( ) => { await ignore404Error ( context , async ( ) => { logEntries = ( await kuduClient . deployment . getLogEntry ( deploymentId ) ) . reverse ( ) ; } ) ; } ) ; let lastLogTimeForThisPoll ; while ( true ) { const newEntry = logEntries . pop ( ) ; if ( ! newEntry ) { break ; } if ( newEntry . message && newEntry . logTime && newEntry . logTime > lastLogTime ) { fullLog = fullLog . concat ( newEntry . message ) ; ext . outputChannel . appendLog ( newEntry . message , { date : newEntry . logTime , resourceName : site . fullName } ) ; lastLogTimeForThisPoll = newEntry . logTime ; if ( /error/i . test ( newEntry . message ) ) { lastErrorLine = newEntry . message ; } } await retryKuduCall ( context , 'getLogEntryDetails' , async ( ) => { await ignore404Error ( context , async ( ) => { if ( newEntry . id && newEntry . detailsUrl ) { const details = await kuduClient . deployment . getLogEntryDetails ( deploymentId , newEntry . id ) ; logEntries . push ( ... cleanDetails ( details ) ) ; } } ) ; } ) ; } if ( lastLogTimeForThisPoll ) { lastLogTime = lastLogTimeForThisPoll ; } if ( deployment . complete ) { if ( deployment . status === 3 || deployment . isTemp ) { const message = localize ( 'deploymentFailed' , 'Deployment to "{0}" failed.' , site . fullName ) ; const viewOutput = localize ( 'viewOutput' , 'View Output' ) ; void window . showErrorMessage ( message , viewOutput ) . then ( result => { if ( result === viewOutput ) { ext . outputChannel . show ( ) ; } } ) ; const messageWithoutName = localize ( 'deploymentFailedWithoutName' , 'Deployment failed.' ) ; ext . outputChannel . appendLog ( messageWithoutName , { resourceName : site . fullName } ) ; context . errorHandling . suppressDisplay = true ; context . telemetry . properties . deployErrorLastLine = lastErrorLine ; throw new Error ( messageWithoutName ) ; } else { context . syncTriggersPostDeploy = site . isFunctionApp && ! /syncing/i . test ( fullLog ) && ! site . isKubernetesApp && ! site . isWorkflowApp ; return ; } } else { await delay ( pollingInterval ) ; } } } async function tryGetLatestDeployment ( context , kuduClient , permanentId , initialStartTime , expectedId ? ) { let deployment ; if ( permanentId ) { deployment = await retryKuduCall ( context , 'getResult' , async ( ) => { return await kuduClient . deployment . getResult ( permanentId ! ) ; } ) ; } else if ( expectedId ) { try { const latestDeployment = await retryKuduCall ( context , 'getResult' , async ( ) => { return await kuduClient . deployment . getResult ( 'latest' ) ; } ) ; if ( latestDeployment . isTemp ) { deployment = latestDeployment ; } else if ( latestDeployment . id === expectedId ) { deployment = latestDeployment ; permanentId = latestDeployment . id ; } } catch ( error ) { const parsedError = parseError ( error ) ; if ( parsedError . errorType !== '404' ) { throw parsedError ; } } } else if ( initialStartTime ) { const deployments = await retryKuduCall ( context , 'getDeployResults' , async ( ) => { return await kuduClient . deployment . getDeployResults ( ) ; } ) ; deployment = deployments . filter ( deployResult => deployResult . startTime && deployResult . startTime >= initialStartTime ! ) . sort ( ( a , b ) => nonNullProp ( b , 'startTime' ) . valueOf ( ) - nonNullProp ( a , 'startTime' ) . valueOf ( ) ) . shift ( ) ; if ( deployment && ! deployment . isTemp ) { permanentId = deployment . id ; } } else { try { deployment = await retryKuduCall ( context , 'getResult' , async ( ) => { return await kuduClient . deployment . getResult ( 'latest' ) ; } ) ; } catch ( error ) { const parsedError = parseError ( error ) ; if ( parsedError . errorType !== '404' ) { throw error ; } } if ( deployment && deployment . startTime ) { initialStartTime = deployment . startTime ; } } return [ deployment , permanentId , initialStartTime ] ; } /**
 * Workaround for some weird Oryx behavior where it shows several duplicate logs on a single line
 */ function cleanDetails ( entries ) { const result = [ ] ; for ( const entry of entries ) { const oryxEOL = '\\n' ; if ( entry . message ?. includes ( oryxEOL ) ) { const newMessages = entry . message . split ( oryxEOL ) ; for ( const newMessage of newMessages ) { if ( ! entries . find ( d => d . message === newMessage ) ) { result . push ( { ... entry , message : newMessage } ) ; } } } else { result . push ( entry ) ; } } return result . reverse ( ) ; }