diff --git a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs index c58a4d0798e..51e669aa030 100644 --- a/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs +++ b/src/Build/BackEnd/Components/ProjectCache/ProjectCacheService.cs @@ -24,6 +24,7 @@ using Microsoft.Build.Graph; using Microsoft.Build.Internal; using Microsoft.Build.Shared; +using Microsoft.Build.Framework.Telemetry; using ExceptionHandling = Microsoft.Build.Framework.ExceptionHandling; #pragma warning disable CS0618 // Type or member is obsolete, this class is adapting to both Experimental and new plugin APIs @@ -816,9 +817,32 @@ public async Task HandleBuildResultAsync( requestConfiguration.RetrieveFromCache(); } - // Now we are sure the Project property is available, verify it's not null before proceeding. - // If it's null, it means the configuration is not properly loaded, which should not happen at this stage. - ErrorUtilities.VerifyThrowInternalNull(requestConfiguration.Project, nameof(requestConfiguration.Project)); + // The Project property may be null if the configuration was created from a remote node + // request or the project was never loaded locally (e.g., in VS scenario where ShouldUseCache + // returns true before checking IsLoaded). Skip cache result handling in this case. + if (requestConfiguration.Project is null) + { + string projectCacheState = string.Join(":", + requestConfiguration.ConfigurationId, + Path.GetFileName(requestConfiguration.ProjectFullPath), + requestConfiguration.IsLoaded, + requestConfiguration.IsCached, + requestConfiguration.WasGeneratedByNode, + _isVsScenario, + _globalProjectCacheDescriptor is not null); + + CrashTelemetryRecorder.EmitEndBuildHangDiagnostics(new CrashTelemetry + { + ExitType = CrashExitType.ProjectCacheFailure, + ExceptionMessage = "Project unexpectedly null in HandleBuildResultAsync", + BuildEngineVersion = Evaluation.ProjectCollection.Version?.ToString(), + BuildEngineFrameworkName = NativeMethodsShared.FrameworkName, + BuildEngineHost = BuildEnvironmentState.GetHostName(), + IsStandaloneExecution = false, + ProjectCacheState = projectCacheState, + }); + return; + } // Filter to plugins which apply to the project, if any List projectCacheDescriptors = GetProjectCacheDescriptors(requestConfiguration.Project).ToList(); diff --git a/src/Framework/Telemetry/CrashTelemetry.cs b/src/Framework/Telemetry/CrashTelemetry.cs index e5314ae9c7c..cc4d4948346 100644 --- a/src/Framework/Telemetry/CrashTelemetry.cs +++ b/src/Framework/Telemetry/CrashTelemetry.cs @@ -353,6 +353,12 @@ internal class CrashTelemetry : TelemetryBase, IActivityTelemetryDataHolder /// public string? ActiveNodeDetails { get; set; } + /// + /// Diagnostic state of the project cache configuration when Project is unexpectedly null. + /// Format: "configId:projectFileName:IsLoaded:IsCached:WasGeneratedByNode:IsVsScenario:HasGlobalPlugins". + /// + public string? ProjectCacheState { get; set; } + // --- Build state diagnostic properties (help diagnose the context of the crash) --- /// @@ -533,6 +539,7 @@ public Dictionary GetActivityProperties() AddIfNotNull(ActiveNodeIds); AddIfNotNull(EnableNodeReuse); AddIfNotNull(ActiveNodeDetails); + AddIfNotNull(ProjectCacheState); // Build state diagnostic properties AddIfNotNull(IsStandaloneExecution); @@ -602,6 +609,7 @@ public override IDictionary GetProperties() AddIfNotNull(ActiveNodeIds); AddIfNotNull(EnableNodeReuse?.ToString(), nameof(EnableNodeReuse)); AddIfNotNull(ActiveNodeDetails); + AddIfNotNull(ProjectCacheState); // Build state diagnostic properties AddIfNotNull(IsStandaloneExecution?.ToString(), nameof(IsStandaloneExecution));