import * as fse from 'fs-extra' ; import * as os from 'os' ; import * as path from 'path' ; import { CancellationToken , CompletionContext , CompletionItem , CompletionList , Diagnostic , Event , EventEmitter , Position , ProgressLocation , TextDocument , Uri , window , workspace } from 'vscode' ; import { callWithTelemetryAndErrorHandling , callWithTelemetryAndErrorHandlingSync , IActionContext , ITelemetryContext , parseError } from 'vscode-azureextensionui' ; import { LanguageClient , LanguageClientOptions , RevealOutputChannelOn , ServerOptions } from 'vscode-languageclient/node' ; import { armTemplateLanguageId , backendValidationDiagnosticsSource , configKeys , configPrefix , downloadDotnetVersion , isRunningTests , languageFriendlyName , languageServerFolderName , languageServerName , notifications } from '../../common' ; import { delay } from '../../test/support/delay' ; import { acquireSharedDotnetInstallation } from '../acquisition/acquireSharedDotnetInstallation' ; import { convertDiagnosticUrisToLinkedTemplateSchema , INotifyTemplateGraphArgs , IRequestOpenLinkedFileArgs , onRequestOpenLinkedFile } from '../documents/templates/linkedTemplates/linkedTemplates' ; import { templateDocumentSelector } from '../documents/templates/supported' ; import { ext } from '../extensionVariables' ; import { assert } from '../fixed_assert' ; import { assertNever } from '../util/assertNever' ; import { delayWhileSync } from '../util/delayWhileSync' ; import { WrappedErrorHandler } from './WrappedErrorHandler' ; const languageServerDllName = 'Microsoft.ArmLanguageServer.dll' ; const defaultTraceLevel = 'Warning' ; const _notifyTemplateGraphAvailableEmitter = new EventEmitter < INotifyTemplateGraphArgs & ITelemetryContext > ( ) ; let haveFirstSchemasStartedLoading = false ; let haveFirstSchemasFinishedLoading = false ; let isShowingLoadingSchemasProgress = false ; export enum LanguageServerState { NotStarted , Starting , Failed , Running , Stopped , LoadingSchemas } /**
 * An event that fires when the language server notifies us of the current full template graph of a root template
 */ export const notifyTemplateGraphAvailable = _notifyTemplateGraphAvailableEmitter . event ; export async function stopArmLanguageServer ( ) { if ( ext . languageServerState === LanguageServerState . NotStarted || ext . languageServerState === LanguageServerState . Stopped ) { ext . outputChannel . appendLine ( `${ languageServerName } already stopped...` ) ; return ; } ext . outputChannel . appendLine ( `Stopping ${ languageServerName }...` ) ; ext . languageServerState = LanguageServerState . Stopped ; if ( ext . languageServerClient ) { let client = ext . languageServerClient ; ext . languageServerClient = undefined ; await client . stop ( ) ; } ext . outputChannel . appendLine ( "Language server stopped" ) ; } export function startArmLanguageServerInBackground ( ) { console . log ( ">>> startArmLanguageServerInBackground" ) ; switch ( ext . languageServerState ) { case LanguageServerState . Running : case LanguageServerState . Starting : case LanguageServerState . LoadingSchemas : return ; case LanguageServerState . Failed : case LanguageServerState . NotStarted : case LanguageServerState . Stopped : break ; default : assertNever ( ext . languageServerState ) ; } ext . languageServerState = LanguageServerState . Starting ; window . withProgress ( { location : ProgressLocation . Notification , title : `Starting ${ languageServerName }` } , async ( ) => { try { await callWithTelemetryAndErrorHandling ( 'startArmLanguageServer' , async ( actionContext ) => { try { let serverDllPath = findLanguageServer ( ) ; let dotnetExePath = await getDotNetPath ( ) ; if ( ! dotnetExePath ) { ext . languageServerStartupError = ".dotnet acquisition returned no path" ; ext . languageServerState = LanguageServerState . Failed ; return ; } await startLanguageClient ( serverDllPath , dotnetExePath ) ; ext . languageServerState = haveFirstSchemasFinishedLoading ? LanguageServerState . Running : LanguageServerState . LoadingSchemas ; } catch ( error ) { ext . languageServerStartupError = `${ parseError ( error ) . message }: ${ error instanceof Error ? error . stack : 'no stack' }` ; ext . languageServerState = LanguageServerState . Failed ; throw error ; } } ) ; } catch ( err ) { assert . fail ( "callWithTelemetryAndErrorHandling in startArmLanguageServerInBackground onTemplateGraphAvailable shouldn't throw" ) ; } } ) ; } async function getLangServerVersion ( ) { return await callWithTelemetryAndErrorHandling ( 'getLangServerVersion' , async ( actionContext ) => { actionContext . errorHandling . suppressDisplay = true ; actionContext . telemetry . suppressIfSuccessful = true ; const packagePath = ext . context . asAbsolutePath ( 'package.json' ) ; const packageContents = < { config : { ARM_LANGUAGE_SERVER_NUGET_VERSION : string ; } ; } > await fse . readJson ( packagePath ) ; return packageContents . config . ARM_LANGUAGE_SERVER_NUGET_VERSION ; } ) ; } async function startLanguageClient ( serverDllPath , dotnetExePath ) { await callWithTelemetryAndErrorHandling ( 'startArmLanguageClient' , async ( actionContext ) => { actionContext . errorHandling . rethrow = true ; actionContext . errorHandling . suppressDisplay = true ; let trace = workspace . getConfiguration ( configPrefix ) . get < string > ( configKeys . traceLevel ) || defaultTraceLevel ; let commonArgs = [ serverDllPath , '--logLevel' , trace ] ; const waitForDebugger = workspace . getConfiguration ( configPrefix ) . get < boolean > ( configKeys . waitForDebugger , false ) === true ; if ( waitForDebugger ) { commonArgs . push ( '--wait-for-debugger' ) ; } if ( isRunningTests || ext . addCompletedDiagnostic ) { commonArgs . push ( '--verbose-diagnostics' ) ; } let serverOptions = { run : { command : dotnetExePath , args : commonArgs , options : { shell : false } } , debug : { command : dotnetExePath , args : commonArgs , options : { shell : false } } , } ; let clientOptions = { documentSelector : templateDocumentSelector , diagnosticCollectionName : `${ languageServerName } diagnostics` , outputChannel : ext . outputChannel , revealOutputChannelOn : RevealOutputChannelOn . Error , synchronize : { configurationSection : configPrefix } , middleware : { handleDiagnostics : ( uri , diagnostics , next ) : void => { for ( const d of diagnostics ) { if ( d . source === backendValidationDiagnosticsSource ) { convertDiagnosticUrisToLinkedTemplateSchema ( d ) ; } } next ( uri , diagnostics ) ; } , provideCompletionItem : async ( document , position , context , token , next ) : Promise < undefined | null | CompletionItem [ ] | CompletionList > => { let result = await next ( document , position , context , token ) ; if ( result ) { let items = result ; let isIncomplete = false ; if ( items instanceof CompletionList ) { isIncomplete = items . isIncomplete ?? false ; items = items . items ; } if ( items . every ( item => typeof item . label === "string" && isApiVersion ( item . label ) ) ) { const countItems = items . length ; items = items . map ( ( ci , index ) => { if ( ! ci . sortText ) { let sortText = ( countItems - index ) . toString ( 10 ) ; sortText = sortText . padStart ( 10 - sortText . length , '0' ) ; ci . sortText = sortText ; } return ci ; } ) ; result = new CompletionList ( items , isIncomplete ) ; } } return result ; } , } } ; const langServerVersion = ( await getLangServerVersion ( ) ) || "Unknown" ; actionContext . telemetry . properties . langServerNugetVersion = langServerVersion ; ext . outputChannel . appendLine ( `Starting ${ languageServerName } at ${ serverDllPath }` ) ; ext . outputChannel . appendLine ( `Language server nuget version: ${ langServerVersion }` ) ; ext . outputChannel . appendLine ( `Client options:${ os . EOL }${ JSON . stringify ( clientOptions , undefined , 2 ) }` ) ; ext . outputChannel . appendLine ( `Server options:${ os . EOL }${ JSON . stringify ( serverOptions , undefined , 2 ) }` ) ; let client = new LanguageClient ( armTemplateLanguageId , languageFriendlyName , serverOptions , clientOptions ) ; let defaultHandler = client . createDefaultErrorHandler ( ) ; client . clientOptions . errorHandler = new WrappedErrorHandler ( defaultHandler ) ; if ( waitForDebugger ) { window . showWarningMessage ( `The ${ configPrefix }.languageServer.waitForDebugger option is set.  The language server will pause on startup until a debugger is attached.` ) ; } client . onTelemetry ( ( telemetryData ) => { const eventName = telemetryData . eventName . replace ( /^\/|\/$/g , "" ) ; const fullEventName = `langserver/${ eventName }` ; if ( sanitizeTelemetryData ( fullEventName , telemetryData . properties ) ) { callWithTelemetryAndErrorHandlingSync ( fullEventName , telemetryActionContext => { telemetryActionContext . errorHandling . suppressDisplay = true ; for ( let prop of Object . getOwnPropertyNames ( telemetryData . properties ) ) { const value = telemetryData . properties [ prop ] ; prop = prop . replace ( /^\./g , "" ) ; telemetryActionContext . telemetry . properties [ prop ] = String ( value ) ; } if ( telemetryActionContext . telemetry . properties . error ) { telemetryActionContext . telemetry . properties . result = 'Failed' ; } } ) ; } } ) ; try { let disposable = client . start ( ) ; ext . context . subscriptions . push ( disposable ) ; await client . onReady ( ) ; ext . languageServerClient = client ; client . onRequest ( notifications . requestOpenLinkedTemplate , async ( args ) => { return onRequestOpenLinkedFile ( args ) ; } ) ; client . onNotification ( notifications . notifyTemplateGraph , async ( args ) => { onNotifyTemplateGraph ( args ) ; } ) ; client . onNotification ( notifications . schemaValidationNotification , async ( args ) => { onSchemaValidationNotication ( args ) ; } ) ; } catch ( error ) { throw new Error ( `${ languageServerName }: An error occurred starting the language server.${ os . EOL }${ os . EOL }${ parseError ( error ) . message }` ) ; } } ) ; } function sanitizeTelemetryData ( fullEventName , properties ) { switch ( fullEventName ) { case 'langserver/VS/WebTools/Languages/JSON/UnrecognizedResourceApiVersion' : if ( ! /^[0-9]{4}-[0-9]{2}-[0-9]{2}(-(alpha|beta|preview)+)?$/i . test ( properties [ 'vS.WebTools.Languages.JSON.apiVersion' ] ?? '' ) ) { { return false ; } } properties [ 'vS.WebTools.Languages.JSON.apiVersion' ] = properties [ 'vS.WebTools.Languages.JSON.apiVersion' ] ?. toLowerCase ( ) ; properties [ 'vS.WebTools.Languages.JSON.type' ] = properties [ 'vS.WebTools.Languages.JSON.type' ] ?. replace ( '/' , '@' ) . toLowerCase ( ) ; return true ; default : return true ; } } async function getDotNetPath ( ) { return await callWithTelemetryAndErrorHandling ( "getDotNetPath" , async ( actionContext ) => { actionContext . errorHandling . rethrow = true ; actionContext . errorHandling . suppressDisplay = true ; let dotnetPath ; const overriddenDotNetExePath = workspace . getConfiguration ( configPrefix ) . get < string > ( configKeys . dotnetExePath ) ; if ( typeof overriddenDotNetExePath === "string" && ! ! overriddenDotNetExePath ) { ext . outputChannel . appendLine ( `WARNING: ${ configPrefix }.${ configKeys . dotnetExePath } is set. ` + `This overrides the automatic download and usage of the dotnet runtime and should only be used to work around dotnet installation issues. ` + `If you encounter problems, please try clearing this setting.` ) ; ext . outputChannel . appendLine ( "" ) ; if ( ! ( await isFile ( overriddenDotNetExePath ) ) ) { throw new Error ( `Invalid path given for ${ configPrefix }.${ configKeys . dotnetExePath } setting. Must point to dotnet executable. Could not find file ${ overriddenDotNetExePath }` ) ; } dotnetPath = overriddenDotNetExePath ; actionContext . telemetry . properties . overriddenDotNetExePath = "true" ; } else { actionContext . telemetry . properties . overriddenDotNetExePath = "false" ; dotnetPath = await acquireSharedDotnetInstallation ( downloadDotnetVersion ) ; if ( ! dotnetPath ) { actionContext . errorHandling . suppressDisplay = true ; actionContext . errorHandling . rethrow = false ; throw new Error ( "acquireSharedDotnetInstallation failed (didn't return a path)" ) ; } if ( ! ( await isFile ( dotnetPath ) ) ) { throw new Error ( `The path returned for .net core does not exist: ${ dotnetPath }` ) ; } try { const versionMatch = dotnetPath . match ( /dotnet[\\/]([^\\/]+)[\\/]/ ) ; const actualVersion = versionMatch && versionMatch [ 1 ] || 'unknown' ; actionContext . telemetry . properties . dotnetVersionInstalled = actualVersion ; } catch ( error ) { } } ext . outputChannel . appendLine ( `Using dotnet core executable at ${ dotnetPath }` ) ; return dotnetPath ; } ) ; } function findLanguageServer ( ) { const serverDllPath = callWithTelemetryAndErrorHandlingSync ( 'findLanguageServer' , ( actionContext ) => { actionContext . errorHandling . rethrow = true ; actionContext . errorHandling . suppressDisplay = true ; let serverDllPathSetting = workspace . getConfiguration ( configPrefix ) . get < string | undefined > ( configKeys . langServerPath ) ; if ( typeof serverDllPathSetting !== 'string' || serverDllPathSetting === '' ) { actionContext . telemetry . properties . customServerDllPath = 'false' ; const serverFolderPath = ext . context . asAbsolutePath ( languageServerFolderName ) ; const fullPath = path . join ( serverFolderPath , languageServerDllName ) ; if ( ! fse . existsSync ( serverFolderPath ) || ! fse . existsSync ( fullPath ) ) { throw new Error ( `Cannot find the ${ languageServerName } at ${ fullPath }. Only template string expression functionality will be available.` ) ; } return fullPath ; } else { actionContext . telemetry . properties . customServerDllPath = 'true' ; let fullPath = serverDllPathSetting . trim ( ) ; if ( fse . statSync ( fullPath ) . isDirectory ( ) ) { fullPath = path . join ( fullPath , languageServerDllName ) ; } if ( ! fse . existsSync ( fullPath ) ) { throw new Error ( `Couldn't find the ${ languageServerName } at ${ fullPath }.  Please verify or remove your '${ configPrefix }.languageServer.path' setting.` ) ; } window . showInformationMessage ( `Using custom path for ${ languageServerName }: ${ fullPath }` ) ; return fullPath ; } } ) ; assert ( typeof serverDllPath === "string" , "Should have thrown instead of returning undefined" ) ; return serverDllPath ! ; } async function isFile ( pathPath ) { return ( await fse . pathExists ( pathPath ) ) && ( await fse . stat ( pathPath ) ) . isFile ( ) ; } param sourceTemplateUri param requestedLinkPath function onNotifyTemplateGraph ( args ) { callWithTelemetryAndErrorHandlingSync ( 'notifyTemplateGraph' , async ( context ) => { context . telemetry . suppressIfSuccessful = true ; _notifyTemplateGraphAvailableEmitter . fire ( < INotifyTemplateGraphArgs & ITelemetryContext > Object . assign ( { } , context . telemetry , args ) ) ; } ) ; } function onSchemaValidationNotication ( args ) { if ( ! haveFirstSchemasStartedLoading ) { haveFirstSchemasStartedLoading = true ; } if ( args . completed && ! haveFirstSchemasFinishedLoading ) { haveFirstSchemasFinishedLoading = true ; } const isLoadingSchemas = haveFirstSchemasStartedLoading && ! haveFirstSchemasFinishedLoading ; const newState = ( isLoadingSchemas && ext . languageServerState === LanguageServerState . Running ) ? LanguageServerState . LoadingSchemas : ( ! isLoadingSchemas && ext . languageServerState === LanguageServerState . LoadingSchemas ) ? LanguageServerState . Running : ext . languageServerState ; ext . languageServerState = newState ; if ( newState === LanguageServerState . LoadingSchemas ) { showLoadingSchemasProgress ( ) ; } } function showLoadingSchemasProgress ( ) { if ( ! isShowingLoadingSchemasProgress ) { isShowingLoadingSchemasProgress = true ; window . withProgress ( { location : ProgressLocation . Notification , title : `Loading ARM schemas` } , async ( ) => delayWhileSync ( 500 , ( ) => ext . languageServerState === LanguageServerState . LoadingSchemas ) ) . then ( ( ) => { isShowingLoadingSchemasProgress = false ; } ) ; } } export async function waitForLanguageServerAvailable ( ) { const currentState = ext . languageServerState ; switch ( currentState ) { case LanguageServerState . Stopped : { const msg = 'Cannot start language server. It is in a stopped state.' ; ext . outputChannel . appendLine ( msg ) ; throw new Error ( msg ) ; } case LanguageServerState . Failed : { const msg = `Language server failed on start-up: ${ ext . languageServerStartupError }` ; ext . outputChannel . appendLine ( msg ) ; throw new Error ( msg ) ; } case LanguageServerState . NotStarted : startArmLanguageServerInBackground ( ) ; break ; case LanguageServerState . Running : return ; case LanguageServerState . LoadingSchemas : case LanguageServerState . Starting : break ; default : assertNever ( currentState ) ; } while ( true ) { switch ( ext . languageServerState ) { case LanguageServerState . Failed : { const msg = `Language server failed on start-up: ${ ext . languageServerStartupError }` ; ext . outputChannel . appendLine ( msg ) ; throw new Error ( msg ) ; } case LanguageServerState . NotStarted : case LanguageServerState . Starting : case LanguageServerState . LoadingSchemas : await delay ( 100 ) ; break ; case LanguageServerState . Running : await delay ( 1000 ) ; ext . outputChannel . appendLine ( "Language server is ready." ) ; assert ( ext . languageServerClient ) ; return ; case LanguageServerState . Stopped : { const msg = 'Language server has stopped' ; throw new Error ( msg ) ; } default : assertNever ( ext . languageServerState ) ; } } } function isApiVersion ( s ) { return /^[0-9]{4}-[0-9]{2}-[0-9]{2}(-[0-9a-zA-Z]+)?$/ . test ( s ) ; }