diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs
index 3abc9faaaab..cd7f718ae63 100644
--- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs
+++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Concurrent;
+using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
@@ -69,6 +70,8 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket
///
private bool _cancelRequested = false;
private string _serverBusyMutexName = default!;
+ private CultureInfo _originalCulture = default!;
+ private CultureInfo _originalUICulture = default!;
public OutOfProcServerNode(BuildCallback buildFunction)
{
@@ -93,6 +96,9 @@ public OutOfProcServerNode(BuildCallback buildFunction)
/// The reason for shutting down.
public NodeEngineShutdownReason Run(out Exception? shutdownException)
{
+ _originalCulture = CultureInfo.CurrentCulture;
+ _originalUICulture = CultureInfo.CurrentUICulture;
+
ServerNodeHandshake handshake = new(
CommunicationsUtilities.GetHandshakeOptions(taskHost: false, taskHostParameters: TaskHostParameters.Empty, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture()));
@@ -363,6 +369,19 @@ private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command)
}
private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command)
+ {
+ try
+ {
+ HandleServerNodeBuildCommandCore(command);
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = _originalCulture;
+ Thread.CurrentThread.CurrentUICulture = _originalUICulture;
+ }
+ }
+
+ private void HandleServerNodeBuildCommandCore(ServerNodeBuildCommand command)
{
CommunicationsUtilities.Trace($"Building with MSBuild server with command line {command.CommandLine}");
using var serverBusyMutex = ServerNamedMutex.OpenOrCreateMutex(name: _serverBusyMutexName, createdNew: out var holdsMutex);
diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs
index 1531c665f54..87f212b8576 100644
--- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs
+++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
@@ -55,6 +56,18 @@ public override bool Execute()
}
}
+ public class CultureInfoTask : Microsoft.Build.Utilities.Task
+ {
+ [Required]
+ public string OutputFile { get; set; } = string.Empty;
+
+ public override bool Execute()
+ {
+ File.WriteAllText(OutputFile, string.Join("|", Process.GetCurrentProcess().Id, CultureInfo.CurrentCulture.Name, CultureInfo.CurrentUICulture.Name));
+ return true;
+ }
+ }
+
public class MSBuildServer_Tests : IDisposable
{
private readonly ITestOutputHelper _output;
@@ -78,6 +91,13 @@ public class MSBuildServer_Tests : IDisposable
";
+ private static string cultureInfoTaskContents = @$"
+
+
+
+
+
+";
public MSBuildServer_Tests(ITestOutputHelper output)
{
@@ -136,6 +156,43 @@ public void MSBuildServerTest()
pidOfServerProcess.ShouldNotBe(newServerProcessId, "Node used by both the first and second build should not be the same.");
}
+ [Fact]
+ public void ServerBuildsUseCultureFromEachRequest()
+ {
+ TransientTestFile project = _env.CreateFile("cultureProject.proj", cultureInfoTaskContents);
+ TransientTestFile firstCultureOutput = _env.ExpectFile();
+ TransientTestFile secondCultureOutput = _env.ExpectFile();
+ CultureInfo originalCulture = CultureInfo.CurrentCulture;
+ CultureInfo originalUICulture = CultureInfo.CurrentUICulture;
+
+ try
+ {
+ MSBuildClient.ShutdownServer(CancellationToken.None).ShouldBeTrue();
+
+ MSBuildClientExitResult firstResult = ExecuteServerBuildWithCulture(project.Path, firstCultureOutput.Path, new CultureInfo("en-US"));
+ firstResult.MSBuildClientExitType.ShouldBe(MSBuildClientExitType.Success);
+ firstResult.MSBuildAppExitTypeString.ShouldBe("Success");
+ CultureRecord firstCultureRecord = ReadCultureRecord(firstCultureOutput.Path);
+ _env.WithTransientProcess(firstCultureRecord.ProcessId);
+ firstCultureRecord.Culture.ShouldBe("en-US");
+ firstCultureRecord.UICulture.ShouldBe("en-US");
+
+ MSBuildClientExitResult secondResult = ExecuteServerBuildWithCulture(project.Path, secondCultureOutput.Path, new CultureInfo("fr-FR"));
+ secondResult.MSBuildClientExitType.ShouldBe(MSBuildClientExitType.Success);
+ secondResult.MSBuildAppExitTypeString.ShouldBe("Success");
+ CultureRecord secondCultureRecord = ReadCultureRecord(secondCultureOutput.Path);
+ secondCultureRecord.ProcessId.ShouldBe(firstCultureRecord.ProcessId, "The second build should reuse the same server process.");
+ secondCultureRecord.Culture.ShouldBe("fr-FR");
+ secondCultureRecord.UICulture.ShouldBe("fr-FR");
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = originalCulture;
+ Thread.CurrentThread.CurrentUICulture = originalUICulture;
+ MSBuildClient.ShutdownServer(CancellationToken.None);
+ }
+ }
+
[Fact]
public void VerifyMixedLegacyBehavior()
{
@@ -349,6 +406,35 @@ public void PropertyMSBuildStartupDirectoryOnServer()
output.ShouldContain($@":MSBuildStartupDirectory:{Environment.CurrentDirectory}:");
}
+ private static MSBuildClientExitResult ExecuteServerBuildWithCulture(string projectPath, string outputFile, CultureInfo culture)
+ {
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = culture;
+
+ MSBuildClient client = new(
+ [
+ BuildEnvironmentHelper.Instance.CurrentMSBuildExePath,
+ projectPath,
+ "/t:RecordCulture",
+ $"/p:CultureOutputFile={outputFile}",
+ "/nologo",
+ "/v:m",
+ ],
+ BuildEnvironmentHelper.Instance.CurrentMSBuildExePath);
+
+ return client.Execute(CancellationToken.None);
+ }
+
+ private static CultureRecord ReadCultureRecord(string path)
+ {
+ string[] parts = File.ReadAllText(path).Split('|');
+ parts.Length.ShouldBe(3);
+
+ return new CultureRecord(int.Parse(parts[0], CultureInfo.InvariantCulture), parts[1], parts[2]);
+ }
+
+ private readonly record struct CultureRecord(int ProcessId, string Culture, string UICulture);
+
private int ParseNumber(string searchString, string toFind)
{
Regex regex = new(@$"{toFind}(\d+)");