Skip to content

Commit f2fac18

Browse files
johnml1135claude
andauthored
Fix RegFree manifest-file race under parallel MSBuild (#988)
Fix RegFree manifest race under parallel MSBuild Serialize manifest updates by output path so parallel build workers cannot corrupt a shared file. Add concurrency regression coverage. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 5049347 commit f2fac18

2 files changed

Lines changed: 303 additions & 152 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) 2026 SIL International
2+
// This software is licensed under the LGPL, version 2.1 or later
3+
// (http://www.gnu.org/licenses/lgpl-2.1.html)
4+
5+
using System;
6+
using System.CodeDom.Compiler;
7+
using System.IO;
8+
using System.Linq;
9+
using System.Runtime.InteropServices;
10+
using System.Threading.Tasks;
11+
using System.Xml;
12+
using Microsoft.CSharp;
13+
using NUnit.Framework;
14+
using SIL.FieldWorks.Build.Tasks;
15+
16+
namespace SIL.FieldWorks.Build.Tasks.FwBuildTasksTests
17+
{
18+
/// <summary>
19+
/// Verifies concurrent <see cref="RegFree"/> executions produce a valid shared manifest.
20+
/// </summary>
21+
[TestFixture]
22+
public sealed class RegFreeConcurrencyTests
23+
{
24+
private const string AsmNamespace = "urn:schemas-microsoft-com:asm.v1";
25+
26+
[Test]
27+
public void Execute_ConcurrentInvocationsTargetingSameManifest_AllSucceedAndProduceValidXml()
28+
{
29+
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
30+
Directory.CreateDirectory(tempDir);
31+
var assemblyPath = Path.Combine(tempDir, "RegFreeConcurrencyTestAssembly.dll");
32+
var manifestPath = Path.Combine(tempDir, "Shared.manifest");
33+
34+
try
35+
{
36+
CompileComVisibleAssembly(assemblyPath);
37+
38+
const int concurrentInvocations = 12;
39+
var results = new bool[concurrentInvocations];
40+
var tasks = new Task[concurrentInvocations];
41+
for (var i = 0; i < concurrentInvocations; i++)
42+
{
43+
var index = i;
44+
tasks[index] = Task.Run(() =>
45+
{
46+
var regFree = new RegFree
47+
{
48+
BuildEngine = new FwBuildTasks.TestBuildEngine(),
49+
Executable = assemblyPath,
50+
Output = manifestPath,
51+
ManagedAssemblies = new Microsoft.Build.Utilities.TaskItem[]
52+
{
53+
new Microsoft.Build.Utilities.TaskItem(assemblyPath)
54+
},
55+
Platform = "x64"
56+
};
57+
results[index] = regFree.Execute();
58+
});
59+
}
60+
61+
Task.WaitAll(tasks);
62+
63+
Assert.That(results, Has.All.True,
64+
"every concurrent RegFree.Execute() call must succeed - none should fail with the " +
65+
"manifest-file-in-use IOException this test guards against");
66+
67+
Assert.That(File.Exists(manifestPath), Is.True, "the shared manifest file must exist after all writers finish");
68+
69+
var doc = new XmlDocument();
70+
Assert.DoesNotThrow(() => doc.Load(manifestPath),
71+
"the manifest file must be well-formed XML, not truncated or interleaved by a concurrent write race");
72+
73+
var ns = new XmlNamespaceManager(doc.NameTable);
74+
ns.AddNamespace("asmv1", AsmNamespace);
75+
Assert.That(doc.SelectSingleNode("//asmv1:clrClass", ns), Is.Not.Null,
76+
"the manifest must still contain the expected clrClass content after the concurrent writes");
77+
}
78+
finally
79+
{
80+
if (Directory.Exists(tempDir))
81+
{
82+
Directory.Delete(tempDir, true);
83+
}
84+
}
85+
}
86+
87+
private static void CompileComVisibleAssembly(string outputPath)
88+
{
89+
const string source = @"using System.Runtime.InteropServices;
90+
[assembly: ComVisible(true)]
91+
[assembly: Guid(""6A2C9E1D-6C8B-4B77-9C3E-6F6B6B2C9E1D"")]
92+
namespace RegFreeConcurrencyTestAssembly
93+
{
94+
[ComVisible(true)]
95+
[Guid(""7B3DAF2E-7D9C-4C88-AD4F-7A7C7C3DAF2E"")]
96+
[ProgId(""RegFreeConcurrency.SampleClass"")]
97+
public class SampleComClass
98+
{
99+
}
100+
}";
101+
var provider = new CSharpCodeProvider();
102+
var parameters = new CompilerParameters
103+
{
104+
GenerateExecutable = false,
105+
OutputAssembly = outputPath,
106+
CompilerOptions = "/target:library"
107+
};
108+
parameters.ReferencedAssemblies.Add(typeof(object).Assembly.Location);
109+
parameters.ReferencedAssemblies.Add(typeof(GuidAttribute).Assembly.Location);
110+
111+
var results = provider.CompileAssemblyFromSource(parameters, source);
112+
if (results.Errors.HasErrors)
113+
{
114+
var message = string.Join(Environment.NewLine, results.Errors.Cast<CompilerError>().Select(e => e.ToString()));
115+
throw new InvalidOperationException($"Failed to compile test assembly:{Environment.NewLine}{message}");
116+
}
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)