Skip to content

Commit fa96a2a

Browse files
Merge pull request #6382 from dotnet/dev/kirillo/6379
Fixes #6379 We have a situation where we are in a worker node and a task runs in a separate AppDomain and logs a TaskParameterEventArgs. Since logging is asynchronous there's a risk that by the time the node packet translator accesses the TaskParameterEventArgs.Items the AppDomain is already unloaded and we crash when trying to enumerate item metadata. Detect that we're in another AppDomain and eagerly take a snapshot of task items with all metadata.
2 parents 5b9216a + 8065666 commit fa96a2a

File tree

5 files changed

+190
-8
lines changed

5 files changed

+190
-8
lines changed

src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupLoggingHelper.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Globalization;
8+
using System.Runtime.Remoting;
89
using Microsoft.Build.BackEnd.Logging;
910
using Microsoft.Build.Collections;
1011
using Microsoft.Build.Framework;
@@ -271,6 +272,11 @@ internal static TaskParameterEventArgs CreateTaskParameterEventArgs(
271272
bool logItemMetadata,
272273
DateTime timestamp)
273274
{
275+
// Only create a snapshot of items if we use AppDomains
276+
#if FEATURE_APPDOMAIN
277+
CreateItemsSnapshot(ref items);
278+
#endif
279+
274280
var args = new TaskParameterEventArgs(
275281
messageKind,
276282
itemType,
@@ -281,6 +287,60 @@ internal static TaskParameterEventArgs CreateTaskParameterEventArgs(
281287
return args;
282288
}
283289

290+
#if FEATURE_APPDOMAIN
291+
private static void CreateItemsSnapshot(ref IList items)
292+
{
293+
if (items == null)
294+
{
295+
return;
296+
}
297+
298+
// If we're in the default AppDomain, but any of the items come from a different AppDomain
299+
// we need to take a snapshot of the items right now otherwise that AppDomain might get
300+
// unloaded by the time we want to consume the items.
301+
// If we're not in the default AppDomain, always take the items snapshot.
302+
//
303+
// It is unfortunate to need to be doing this check, but ResolveComReference and other tasks
304+
// still use AppDomains and create a TaskParameterEventArgs in the default AppDomain, but
305+
// pass it Items from another AppDomain.
306+
if (AppDomain.CurrentDomain.IsDefaultAppDomain())
307+
{
308+
bool needsSnapshot = false;
309+
foreach (var item in items)
310+
{
311+
if (RemotingServices.IsTransparentProxy(item))
312+
{
313+
needsSnapshot = true;
314+
break;
315+
}
316+
}
317+
318+
if (!needsSnapshot)
319+
{
320+
return;
321+
}
322+
}
323+
324+
int count = items.Count;
325+
var cloned = new object[count];
326+
327+
for (int i = 0; i < count; i++)
328+
{
329+
var item = items[i];
330+
if (item is ITaskItem taskItem)
331+
{
332+
cloned[i] = new TaskItemData(taskItem);
333+
}
334+
else
335+
{
336+
cloned[i] = item;
337+
}
338+
}
339+
340+
items = cloned;
341+
}
342+
#endif
343+
284344
internal static string GetTaskParameterText(TaskParameterEventArgs args)
285345
=> GetTaskParameterText(args.Kind, args.ItemType, args.Items, args.LogItemMetadata);
286346

src/Build/Instance/ProjectItemInstance.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,16 @@ public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
10681068
var list = _itemDefinitions != null ? MetadataCollection : _directMetadata;
10691069
if (list != null)
10701070
{
1071+
#if FEATURE_APPDOMAIN
1072+
// Can't send a yield-return iterator across AppDomain boundaries
1073+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
1074+
{
1075+
return EnumerateMetadataEager(list);
1076+
}
1077+
#endif
1078+
// Mainline scenario, returns an iterator to avoid allocating an array
1079+
// to store the results. With the iterator, results can stream to the
1080+
// consumer (e.g. binlog writer) without allocations.
10711081
return EnumerateMetadata(list);
10721082
}
10731083
else
@@ -1076,6 +1086,28 @@ public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
10761086
}
10771087
}
10781088

1089+
/// <summary>
1090+
/// Used to return metadata from another AppDomain. Can't use yield return because the
1091+
/// generated state machine is not marked as [Serializable], so we need to allocate.
1092+
/// </summary>
1093+
/// <param name="list">The source list to return metadata from.</param>
1094+
/// <returns>An array of string key-value pairs representing metadata.</returns>
1095+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager(CopyOnWritePropertyDictionary<ProjectMetadataInstance> list)
1096+
{
1097+
var result = new List<KeyValuePair<string, string>>(list.Count);
1098+
1099+
foreach (var projectMetadataInstance in list)
1100+
{
1101+
if (projectMetadataInstance != null)
1102+
{
1103+
result.Add(new KeyValuePair<string, string>(projectMetadataInstance.Name, projectMetadataInstance.EvaluatedValue));
1104+
}
1105+
}
1106+
1107+
// Probably better to send the raw array across the wire even if it's another allocation.
1108+
return result.ToArray();
1109+
}
1110+
10791111
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadata(CopyOnWritePropertyDictionary<ProjectMetadataInstance> list)
10801112
{
10811113
foreach (var projectMetadataInstance in list)
@@ -1152,7 +1184,7 @@ internal CopyOnWritePropertyDictionary<ProjectMetadataInstance> MetadataCollecti
11521184

11531185
IEnumerable<ProjectMetadataInstance> IItem<ProjectMetadataInstance>.Metadata => MetadataCollection;
11541186

1155-
#region Operators
1187+
#region Operators
11561188

11571189
/// <summary>
11581190
/// This allows an explicit typecast from a "TaskItem" to a "string", returning the ItemSpec for this item.
@@ -1193,7 +1225,7 @@ public static explicit operator string(TaskItem that)
11931225
return !(left == right);
11941226
}
11951227

1196-
#endregion
1228+
#endregion
11971229

11981230
/// <summary>
11991231
/// Produce a string representation.
@@ -1215,7 +1247,7 @@ public override object InitializeLifetimeService()
12151247
}
12161248
#endif
12171249

1218-
#region IItem and ITaskItem2 Members
1250+
#region IItem and ITaskItem2 Members
12191251

12201252
/// <summary>
12211253
/// Returns the metadata with the specified key.
@@ -1467,9 +1499,9 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped()
14671499
return clonedMetadata;
14681500
}
14691501

1470-
#endregion
1502+
#endregion
14711503

1472-
#region INodePacketTranslatable Members
1504+
#region INodePacketTranslatable Members
14731505

14741506
/// <summary>
14751507
/// Reads or writes the packet to the serializer.
@@ -1499,9 +1531,9 @@ void ITranslatable.Translate(ITranslator translator)
14991531
}
15001532
}
15011533

1502-
#endregion
1534+
#endregion
15031535

1504-
#region IEquatable<TaskItem> Members
1536+
#region IEquatable<TaskItem> Members
15051537

15061538
/// <summary>
15071539
/// Override of GetHashCode.
@@ -1579,7 +1611,7 @@ public bool Equals(TaskItem other)
15791611
return true;
15801612
}
15811613

1582-
#endregion
1614+
#endregion
15831615

15841616
/// <summary>
15851617
/// Returns true if a particular piece of metadata is defined on this item (even if

src/Framework/TaskItemData.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ public TaskItemData(string itemSpec, IDictionary<string, string> metadata)
2525
Metadata = metadata ?? _emptyMetadata;
2626
}
2727

28+
/// <summary>
29+
/// Clone the task item and all metadata to create a snapshot
30+
/// </summary>
31+
/// <param name="original">An <see cref="ITaskItem"/> to clone</param>
32+
public TaskItemData(ITaskItem original)
33+
{
34+
ItemSpec = original.ItemSpec;
35+
var metadata = original.EnumerateMetadata();
36+
37+
// Can't preallocate capacity because we don't know how large it will get
38+
// without enumerating the enumerable
39+
var dictionary = new Dictionary<string, string>();
40+
foreach (var item in metadata)
41+
{
42+
dictionary.Add(item.Key, item.Value);
43+
}
44+
45+
Metadata = dictionary;
46+
}
47+
2848
IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata() => Metadata;
2949

3050
public int MetadataCount => Metadata.Count;

src/Shared/TaskParameter.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,41 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped()
759759

760760
public IEnumerable<KeyValuePair<string, string>> EnumerateMetadata()
761761
{
762+
#if FEATURE_APPDOMAIN
763+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
764+
{
765+
return EnumerateMetadataEager();
766+
}
767+
#endif
768+
769+
return EnumerateMetadataLazy();
770+
}
771+
772+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager()
773+
{
774+
if (_customEscapedMetadata == null || _customEscapedMetadata.Count == 0)
775+
{
776+
#if TASKHOST
777+
// MSBuildTaskHost.dll compiles against .NET 3.5 which doesn't have Array.Empty()
778+
return new KeyValuePair<string, string>[0];
779+
#else
780+
return Array.Empty<KeyValuePair<string, string>>();
781+
#endif
782+
}
783+
784+
var result = new KeyValuePair<string, string>[_customEscapedMetadata.Count];
785+
int index = 0;
786+
foreach (var kvp in _customEscapedMetadata)
787+
{
788+
var unescaped = new KeyValuePair<string, string>(kvp.Key, EscapingUtilities.UnescapeAll(kvp.Value));
789+
result[index++] = unescaped;
790+
}
791+
792+
return result;
793+
}
794+
795+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataLazy()
796+
{
762797
if (_customEscapedMetadata == null)
763798
{
764799
yield break;

src/Utilities/TaskItem.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,41 @@ IDictionary ITaskItem2.CloneCustomMetadataEscaped() => _metadata == null
465465

466466
IEnumerable<KeyValuePair<string, string>> IMetadataContainer.EnumerateMetadata()
467467
{
468+
#if FEATURE_APPDOMAIN
469+
// Can't send a yield-return iterator across AppDomain boundaries
470+
// so have to allocate
471+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
472+
{
473+
return EnumerateMetadataEager();
474+
}
475+
#endif
476+
477+
// In general case we want to return an iterator without allocating a collection
478+
// to hold the result, so we can stream the items directly to the consumer.
479+
return EnumerateMetadataLazy();
480+
}
481+
482+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataEager()
483+
{
484+
if (_metadata == null)
485+
{
486+
return Array.Empty<KeyValuePair<string, string>>();
487+
}
488+
489+
int count = _metadata.Count;
490+
int index = 0;
491+
var result = new KeyValuePair<string, string>[count];
492+
foreach (var kvp in _metadata)
493+
{
494+
var unescaped = new KeyValuePair<string, string>(kvp.Key, EscapingUtilities.UnescapeAll(kvp.Value));
495+
result[index++] = unescaped;
496+
}
497+
498+
return result;
499+
}
500+
501+
private IEnumerable<KeyValuePair<string, string>> EnumerateMetadataLazy()
502+
{
468503
if (_metadata == null)
469504
{
470505
yield break;

0 commit comments

Comments
 (0)