| File: Language\LanguageService.cs | Web Access |
| Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Aspire.Hosting.RemoteHost.Diagnostics; using Aspire.TypeSystem; using Microsoft.Extensions.Logging; using StreamJsonRpc; namespace Aspire.Hosting.RemoteHost.Language; /// <summary> /// JSON-RPC service for language-specific scaffolding, detection, and runtime configuration. /// </summary> internal sealed class LanguageService { private const string ScaffoldAppHostMethodName = "scaffoldAppHost"; private const string DetectAppHostTypeMethodName = "detectAppHostType"; private const string GetRuntimeSpecMethodName = "getRuntimeSpec"; private readonly JsonRpcAuthenticationState _authenticationState; private readonly LanguageSupportResolver _resolver; private readonly ILogger<LanguageService> _logger; private readonly RemoteHostProfilingTelemetry _profilingTelemetry; public LanguageService( JsonRpcAuthenticationState authenticationState, LanguageSupportResolver resolver, ILogger<LanguageService> logger, RemoteHostProfilingTelemetry profilingTelemetry) { _authenticationState = authenticationState; _resolver = resolver; _logger = logger; _profilingTelemetry = profilingTelemetry; } /// <summary> /// Scaffolds a new AppHost project for the specified language. /// </summary> /// <param name="language">The target language (e.g., "TypeScript", "Python").</param> /// <param name="targetPath">The target directory path for the project.</param> /// <param name="projectName">Optional project name. If null, derived from directory name.</param> /// <returns>A dictionary of relative file paths to file contents.</returns> [JsonRpcMethod(ScaffoldAppHostMethodName)] public Dictionary<string, string> ScaffoldAppHost(string language, string targetPath, string? projectName = null) { using var rpcActivity = _profilingTelemetry.StartJsonRpcServerCall(ScaffoldAppHostMethodName); using var activity = _profilingTelemetry.StartLanguageScaffold(language); try { _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> scaffoldAppHost({Language}, {TargetPath}, {ProjectName})", language, targetPath, projectName); var sw = Stopwatch.StartNew(); var languageSupport = _resolver.GetLanguageSupport(language); if (languageSupport == null) { throw new ArgumentException(BuildNoLanguageSupportMessage(language)); } var request = new ScaffoldRequest { TargetPath = targetPath, ProjectName = projectName }; var files = languageSupport.Scaffold(request); activity.SetFileCount(files.Count); _logger.LogDebug("<< scaffoldAppHost({Language}) completed in {ElapsedMs}ms, generated {FileCount} files", language, sw.ElapsedMilliseconds, files.Count); return files; } catch (Exception ex) { activity.SetError(ex); _logger.LogError(ex, "<< scaffoldAppHost({Language}) failed", language); throw; } } /// <summary> /// Detects the language of an AppHost in the specified directory. /// </summary> /// <param name="directoryPath">The directory to check.</param> /// <returns>Detection result with language and file information.</returns> [JsonRpcMethod(DetectAppHostTypeMethodName)] public DetectionResult DetectAppHostType(string directoryPath) { using var rpcActivity = _profilingTelemetry.StartJsonRpcServerCall(DetectAppHostTypeMethodName); using var activity = _profilingTelemetry.StartLanguageDetect(); try { _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> detectAppHostType({DirectoryPath})", directoryPath); var sw = System.Diagnostics.Stopwatch.StartNew(); foreach (var languageSupport in _resolver.GetAllLanguages()) { var result = languageSupport.Detect(directoryPath); if (result.IsValid) { activity.SetLanguage(result.Language); activity.SetDetectionMatched(true); _logger.LogDebug("<< detectAppHostType({DirectoryPath}) found {Language} in {ElapsedMs}ms", directoryPath, result.Language, sw.ElapsedMilliseconds); return result; } } _logger.LogDebug("<< detectAppHostType({DirectoryPath}) not found in {ElapsedMs}ms", directoryPath, sw.ElapsedMilliseconds); activity.SetDetectionMatched(false); return DetectionResult.NotFound; } catch (Exception ex) { activity.SetError(ex); _logger.LogError(ex, "<< detectAppHostType({DirectoryPath}) failed", directoryPath); throw; } } /// <summary> /// Gets the runtime execution specification for the specified language. /// </summary> /// <param name="language">The target language (e.g., "TypeScript", "Python").</param> /// <returns>The runtime spec containing commands for execution.</returns> [JsonRpcMethod(GetRuntimeSpecMethodName)] public RuntimeSpec GetRuntimeSpec(string language) { using var rpcActivity = _profilingTelemetry.StartJsonRpcServerCall(GetRuntimeSpecMethodName); using var activity = _profilingTelemetry.StartLanguageGetRuntimeSpec(language); try { _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> getRuntimeSpec({Language})", language); var sw = System.Diagnostics.Stopwatch.StartNew(); var languageSupport = _resolver.GetLanguageSupport(language); if (languageSupport == null) { throw new ArgumentException(BuildNoLanguageSupportMessage(language)); } var spec = languageSupport.GetRuntimeSpec(); _logger.LogDebug("<< getRuntimeSpec({Language}) completed in {ElapsedMs}ms", language, sw.ElapsedMilliseconds); return spec; } catch (Exception ex) { activity.SetError(ex); _logger.LogError(ex, "<< getRuntimeSpec({Language}) failed", language); throw; } } private string BuildNoLanguageSupportMessage(string language) { var available = _resolver.GetAllLanguages() .Select(l => l.Language) .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) .ToArray(); if (available.Length == 0) { // No language support discovered at all is almost always a binary-mismatch / // type-load failure (see LanguageSupportResolver warnings). Point the user at // the apphost server log so they can see the underlying LoaderExceptions. return $"No language support found for: {language}. " + "No language support implementations were discovered in any loaded assembly. " + "This usually indicates a binary mismatch between the bundled apphost server and the integration assemblies on disk; " + "check the apphost server log for 'LoaderExceptions' Warnings."; } return $"No language support found for: {language}. Available languages: {string.Join(", ", available)}."; } }