Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,46 @@ public void TestTaskDictionaryOutputItems()
ml.AssertLogContains("a=b");
}

[Theory]
[InlineData(typeof(OutOfMemoryException), true)]
[InlineData(typeof(ArgumentException), false)]
public void TaskExceptionHandlingTest(Type exceptionType, bool isCritical)
{
string testExceptionMessage = "Test Message";
string customTaskPath = Assembly.GetExecutingAssembly().Location;
MockLogger ml = new MockLogger() { AllowTaskCrashes = true };

using TestEnvironment env = TestEnvironment.Create();
var debugFolder = env.CreateFolder(true);
// inject the location for failure logs - not to interact with other tests
env.SetEnvironmentVariable("MSBUILDDEBUGPATH", debugFolder.Path);

ObjectModelHelpers.BuildProjectExpectFailure($"""
<Project ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<UsingTask TaskName=`TaskThatThrows` AssemblyFile=`{customTaskPath}`/>
<Target Name=`Build`>
<TaskThatThrows ExceptionType="{exceptionType.ToString()}" ExceptionMessage="{testExceptionMessage}">
</TaskThatThrows>
</Target>
</Project>
""",
ml);
// 'This is an unhandled exception from a task'
ml.AssertLogContains("MSB4018");
// 'An internal failure occurred while running MSBuild'
ml.AssertLogDoesntContain("MSB1025");
// 'This is an unhandled error in MSBuild'
ml.AssertLogDoesntContain(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("UnhandledMSBuildError", string.Empty));
ml.AssertLogContains(testExceptionMessage);

File.Exists(ExceptionHandling.DumpFilePath).ShouldBe(isCritical,
$"{ExceptionHandling.DumpFilePath} expected to exist: {isCritical}");
if (isCritical)
{
FileUtilities.DeleteNoThrow(ExceptionHandling.DumpFilePath);
}
}

[Fact]
public void TestTaskParameterLogging()
{
Expand Down
33 changes: 33 additions & 0 deletions src/Build.UnitTests/BackEnd/TaskThatThrows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Build.Engine.UnitTests;

/// <summary>
/// Task that throws exception based on input parameters.
/// </summary>
public sealed class TaskThatThrows : Utilities.Task
{
public string? ExceptionType { get; set; }

public string? ExceptionMessage { get; set; }

public override bool Execute()
{
string exceptionMessage = string.IsNullOrWhiteSpace(ExceptionMessage) ? "Default exception message" : ExceptionMessage!;

Type exceptionType = string.IsNullOrWhiteSpace(ExceptionType) ? typeof(Exception) : Type.GetType(ExceptionType) ?? typeof(Exception);

ConstructorInfo? ctor = exceptionType.GetConstructor([typeof(string)]);
Exception exceptionInstance = (Exception)(ctor?.Invoke([exceptionMessage]) ?? new Exception(exceptionMessage));

throw exceptionInstance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ private static void RegisterExceptions()
new(typeof(SchedulerCircularDependencyException), (msg, inner) => new SchedulerCircularDependencyException(msg, inner)),
new(typeof(RegistryException), (msg, inner) => new RegistryException(msg, inner)),
new(typeof(HostObjectException), (msg, inner) => new HostObjectException(msg, inner)),
new(typeof(UnbuildableProjectTypeException), (msg, inner) => new UnbuildableProjectTypeException(msg, inner)));
new(typeof(UnbuildableProjectTypeException), (msg, inner) => new UnbuildableProjectTypeException(msg, inner)),
new(typeof(CriticalTaskException), (msg, inner) => new CriticalTaskException(msg, inner)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -853,7 +853,7 @@ private async Task BuildAndReport()

loggingContext.LogCommentFromText(MessageImportance.Low, ex.ToString());
}
else
else if (ex is not CriticalTaskException)
{
(((LoggingContext)_projectLoggingContext) ?? _nodeLoggingContext).LogError(BuildEventFileInfo.Empty, "UnhandledMSBuildError", ex.ToString());
}
Expand Down
12 changes: 11 additions & 1 deletion src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -814,8 +814,18 @@ private async Task<WorkUnitResult> ExecuteInstantiatedTask(ITaskExecutionHost ta
}
}
}
catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex) && Environment.GetEnvironmentVariable("MSBUILDDONOTCATCHTASKEXCEPTIONS") != "1")
catch (Exception ex)
{
if (ExceptionHandling.IsCriticalException(ex) || Environment.GetEnvironmentVariable("MSBUILDDONOTCATCHTASKEXCEPTIONS") == "1")
{
taskLoggingContext.LogFatalTaskError(
ex,
new BuildEventFileInfo(_targetChildInstance.Location),
_taskNode.Name);

throw new CriticalTaskException(ex);
}

taskException = ex;
}

Expand Down
25 changes: 25 additions & 0 deletions src/Framework/CriticalTaskException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Build.Framework.BuildException;

namespace Microsoft.Build.Framework
{
internal sealed class CriticalTaskException : BuildExceptionBase
{
public CriticalTaskException(
Exception innerException)
: base(string.Empty, innerException)
{ }

// Do not remove - used by BuildExceptionSerializationHelper
internal CriticalTaskException(string message, Exception? inner)
: base(message, inner)
{ }
}
}
8 changes: 8 additions & 0 deletions src/Shared/ExceptionHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ private static string GetDebugDumpPath()
/// </summary>
internal static string DebugDumpPath => s_debugDumpPath;

/// <summary>
/// The file used for diagnostic log files.
/// </summary>
internal static string DumpFilePath => s_dumpFileName;

#if !BUILDINGAPPXTASKS
/// <summary>
/// The filename that exceptions will be dumped to
Expand All @@ -99,6 +104,9 @@ internal static bool IsCriticalException(Exception e)
|| e is ThreadAbortException
|| e is ThreadInterruptedException
|| e is AccessViolationException
#if !TASKHOST
|| e is CriticalTaskException
#endif
#if !BUILDINGAPPXTASKS
|| e is InternalErrorException
#endif
Expand Down