import { ConfigurationChangeEvent , Disposable , workspace } from 'vscode' ; import { IActionContext , parseError } from 'vscode-azureextensionui' ; import { ProjectLanguage , projectTemplateKeySetting , TemplateFilter } from '../constants' ; import { ext , TemplateSource } from '../extensionVariables' ; import { FuncVersion } from '../FuncVersion' ; import { localize } from '../localize' ; import { delay } from '../utils/delay' ; import { nonNullValue } from '../utils/nonNull' ; import { requestUtils } from '../utils/requestUtils' ; import { getWorkspaceSetting } from '../vsCodeConfig/settings' ; import { DotnetTemplateProvider } from './dotnet/DotnetTemplateProvider' ; import { getDotnetVerifiedTemplateIds } from './dotnet/getDotnetVerifiedTemplateIds' ; import { IBindingTemplate } from './IBindingTemplate' ; import { IFunctionTemplate , TemplateCategory } from './IFunctionTemplate' ; import { ITemplates } from './ITemplates' ; import { getJavaVerifiedTemplateIds } from './java/getJavaVerifiedTemplateIds' ; import { JavaTemplateProvider } from './java/JavaTemplateProvider' ; import { getScriptVerifiedTemplateIds } from './script/getScriptVerifiedTemplateIds' ; import { IScriptFunctionTemplate } from './script/parseScriptTemplates' ; import { ScriptBundleTemplateProvider } from './script/ScriptBundleTemplateProvider' ; import { ScriptTemplateProvider } from './script/ScriptTemplateProvider' ; import { TemplateProviderBase } from './TemplateProviderBase' ; type CachedProviders = { providers : TemplateProviderBase [ ] ; templatesTask ? : Promise < ITemplates > ; } ; export class CentralTemplateProvider implements Disposable { public readonly templateSource : TemplateSource | undefined ; private readonly _providersMap = new Map < string , CachedProviders > ( ) ; private _disposables : Disposable [ ] = [ ] ; public constructor ( templateSource ? ) { this . templateSource = templateSource || getWorkspaceSetting ( 'templateSource' ) ; this . _disposables . push ( workspace . onDidChangeConfiguration ( e => this . onConfigChanged ( e ) ) ) ; } public dispose ( ) { const allProviders = [ ] ; for ( const p of this . _providersMap . values ( ) ) { allProviders . push ( ... p . providers ) ; } Disposable . from ( ... allProviders , ... this . _disposables ) . dispose ( ) ; this . _providersMap . clear ( ) ; } public static getProviders ( projectPath , language , version , projectTemplateKey ) { const providers = [ ] ; switch ( language ) { case ProjectLanguage . CSharp : case ProjectLanguage . FSharp : providers . push ( new DotnetTemplateProvider ( version , projectPath , language , projectTemplateKey ) ) ; break ; case ProjectLanguage . Java : providers . push ( new JavaTemplateProvider ( version , projectPath , language , projectTemplateKey ) ) ; break ; default : providers . push ( new ScriptTemplateProvider ( version , projectPath , language , projectTemplateKey ) ) ; if ( version !== FuncVersion . v1 ) { providers . push ( new ScriptBundleTemplateProvider ( version , projectPath , language , projectTemplateKey ) ) ; } break ; } return providers ; } public async getFunctionTemplates ( context , projectPath , language , version , templateFilter , projectTemplateKey ) { const templates = await this . getTemplates ( context , projectPath , language , version , projectTemplateKey ) ; switch ( templateFilter ) { case TemplateFilter . All : return templates . functionTemplates ; case TemplateFilter . Core : return templates . functionTemplates . filter ( ( t ) => t . categories . find ( ( c ) => c === TemplateCategory . Core ) !== undefined ) ; case TemplateFilter . Verified : default : const verifiedTemplateIds = getScriptVerifiedTemplateIds ( version ) . concat ( getDotnetVerifiedTemplateIds ( version ) ) . concat ( getJavaVerifiedTemplateIds ( ) ) ; return templates . functionTemplates . filter ( ( t ) => verifiedTemplateIds . find ( vt => typeof vt === 'string' ? vt === t . id : vt . test ( t . id ) ) ) ; } } public async clearTemplateCache ( context , projectPath , language , version ) { const providers = CentralTemplateProvider . getProviders ( projectPath , language , version , undefined ) ; for ( const provider of providers ) { await provider . clearCachedTemplateMetadata ( ) ; await provider . clearCachedTemplates ( context ) ; provider . projKeyMayHaveChanged ( ) ; } const cachedProviders = this . tryGetCachedProviders ( projectPath , language , version ) ; if ( cachedProviders ) { delete cachedProviders . templatesTask ; } } public async getBindingTemplates ( context , projectPath , language , version ) { const templates = await this . getTemplates ( context , projectPath , language , version , undefined ) ; return templates . bindingTemplates ; } public async tryGetSampleData ( context , version , triggerBindingType ) { try { const templates = < IScriptFunctionTemplate [ ] > await this . getFunctionTemplates ( context , undefined , ProjectLanguage . JavaScript , version , TemplateFilter . All , undefined ) ; const template = templates . find ( t => t . functionJson . triggerBinding ?. type ?. toLowerCase ( ) === triggerBindingType . toLowerCase ( ) ) ; return template ?. templateFiles [ 'sample.dat' ] ; } catch { return undefined ; } } public async getProjectTemplateKey ( context , projectPath , language , version , projectTemplateKey ) { const cachedProviders = await this . getCachedProviders ( context , projectPath , language , version , projectTemplateKey ) ; const provider = nonNullValue ( cachedProviders . providers [ 0 ] , 'firstProvider' ) ; return await provider . getProjKey ( context ) ; } private getCachedProvidersKey ( language , version ) { return language + version ; } private tryGetCachedProviders ( projectPath , language , version ) { const key = this . getCachedProvidersKey ( language , version ) ; if ( this . _providersMap . has ( key ) ) { return this . _providersMap . get ( key ) ; } else if ( projectPath ) { return this . _providersMap . get ( key + projectPath ) ; } else { return undefined ; } } private setCachedProviders ( projectPath , language , version , cachedProviders ) { let key = this . getCachedProvidersKey ( language , version ) ; if ( cachedProviders . providers . some ( p => p . supportsProjKey ( ) ) ) { key += projectPath ; } this . _providersMap . set ( key , cachedProviders ) ; } private async getCachedProviders ( context , projectPath , language , version , projectTemplateKey ) { let cachedProviders = this . tryGetCachedProviders ( projectPath , language , version ) ; if ( ! cachedProviders ) { cachedProviders = { providers : CentralTemplateProvider . getProviders ( projectPath , language , version , projectTemplateKey ) } ; this . setCachedProviders ( projectPath , language , version , cachedProviders ) ; } else { await Promise . all ( cachedProviders . providers . map ( async ( p ) => { if ( await p . updateProjKeyIfChanged ( context , projectTemplateKey ) ) { delete cachedProviders ?. templatesTask ; } } ) ) ; } return cachedProviders ; } /**
     * Ensures we only have one task going at a time for refreshing templates
     */ private async getTemplates ( context , projectPath , language , version , projectTemplateKey ) { context . telemetry . properties . projectRuntime = version ; context . telemetry . properties . projectLanguage = language ; const cachedProviders = await this . getCachedProviders ( context , projectPath , language , version , projectTemplateKey ) ; let templatesTask = cachedProviders . templatesTask ; if ( templatesTask ) { return await templatesTask ; } else { templatesTask = this . refreshTemplates ( context , cachedProviders . providers ) ; cachedProviders . templatesTask = templatesTask ; try { return await templatesTask ; } catch ( error ) { delete cachedProviders . templatesTask ; throw error ; } } } private async refreshTemplates ( context , providers ) { return ( await Promise . all ( providers . map ( async ( provider ) => { return await this . refreshTemplatesForProvider ( context , provider ) ; } ) ) ) . reduce ( ( t1 , t2 ) => { return { functionTemplates : t1 . functionTemplates . concat ( t2 . functionTemplates ) , bindingTemplates : t1 . bindingTemplates . concat ( t2 . bindingTemplates ) } ; } ) ; } private async refreshTemplatesForProvider ( context , provider ) { let result ; let latestErrorMessage ; try { const latestTemplateVersion = await provider . getLatestTemplateVersion ( context ) ; context . telemetry . properties . latestTemplateVersion = latestTemplateVersion ; const cachedTemplateVersion = await provider . getCachedTemplateVersion ( ) ; context . telemetry . properties . cachedTemplateVersion = cachedTemplateVersion ; if ( cachedTemplateVersion === latestTemplateVersion ) { result = await this . tryGetCachedTemplates ( context , provider ) ; } if ( ! result ) { const timeout = requestUtils . getRequestTimeoutMS ( ) ; result = await Promise . race ( [ this . getLatestTemplates ( context , provider , latestTemplateVersion ) , delay ( timeout ) . then ( ( ) => { throw new Error ( localize ( 'templatesTimeout' , 'Retrieving templates timed out. Modify setting "{0}.{1}" if you want to extend the timeout.' , ext . prefix , requestUtils . timeoutKey ) ) ; } ) ] ) ; } } catch ( error ) { const errorMessage = parseError ( error ) . message ; latestErrorMessage = localize ( 'latestTemplatesError' , 'Failed to get latest templates: {0}' , errorMessage ) ; ext . outputChannel . appendLog ( latestErrorMessage ) ; context . telemetry . properties . latestTemplatesError = errorMessage ; } if ( ! result ) { result = await this . tryGetCachedTemplates ( context , provider ) ; } if ( ! result ) { result = await this . tryGetBackupTemplates ( context , provider ) ; } if ( result ) { return { functionTemplates : result . functionTemplates . filter ( f => this . includeTemplate ( provider , f ) ) , bindingTemplates : result . bindingTemplates . filter ( b => this . includeTemplate ( provider , b ) ) } ; } else if ( latestErrorMessage ) { throw new Error ( latestErrorMessage ) ; } else { throw new Error ( localize ( 'templateSourceError' , 'Internal error: Failed to get templates for source "{0}".' , this . templateSource ) ) ; } } private includeTemplate ( provider , template ) { return provider . includeTemplate ( template ) && ( ! ( 'language' in template ) || template . language . toLowerCase ( ) === provider . language . toLowerCase ( ) ) ; } private async getLatestTemplates ( context , provider , latestTemplateVersion ) { if ( ! this . templateSource || this . templateSource === TemplateSource . Latest || this . templateSource === TemplateSource . Staging ) { context . telemetry . properties . templateSource = 'latest' ; const result = await provider . getLatestTemplates ( context , latestTemplateVersion ) ; await provider . cacheTemplateMetadata ( latestTemplateVersion ) ; await provider . cacheTemplates ( context ) ; return result ; } return undefined ; } private async tryGetCachedTemplates ( context , provider ) { if ( ! this . templateSource ) { try { context . telemetry . properties . templateSource = 'cache' ; if ( await provider . doesCachedProjKeyMatch ( context ) ) { return await provider . getCachedTemplates ( context ) ; } else { return undefined ; } } catch ( error ) { const errorMessage = parseError ( error ) . message ; ext . outputChannel . appendLog ( localize ( 'cachedTemplatesError' , 'Failed to get cached templates: {0}' , errorMessage ) ) ; context . telemetry . properties . cachedTemplatesError = errorMessage ; } } return undefined ; } private async tryGetBackupTemplates ( context , provider ) { if ( ! this . templateSource || this . templateSource === TemplateSource . Backup ) { try { context . telemetry . properties . templateSource = 'backup' ; const backupTemplateVersion = await provider . getBackupTemplateVersion ( ) ; context . telemetry . properties . backupTemplateVersion = backupTemplateVersion ; const result = await provider . getBackupTemplates ( context ) ; await provider . cacheTemplateMetadata ( backupTemplateVersion ) ; await provider . cacheTemplates ( context ) ; return result ; } catch ( error ) { const errorMessage = parseError ( error ) . message ; ext . outputChannel . appendLog ( localize ( 'backupTemplatesError' , 'Failed to get backup templates: {0}' , errorMessage ) ) ; context . telemetry . properties . backupTemplatesError = errorMessage ; } } return undefined ; } private onConfigChanged ( e ) { if ( e . affectsConfiguration ( `${ ext . prefix }.${ projectTemplateKeySetting }` ) ) { for ( const cached of this . _providersMap . values ( ) ) { for ( const provider of cached . providers ) { provider . projKeyMayHaveChanged ( ) ; } } } } }