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+)");