Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
Next Next commit
Shutdown build servers via named pipes
  • Loading branch information
jjonescz committed Jul 23, 2025
commit fbd7f810fe3da61f31ae17b001fc5a3034aa27c4
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<PackageVersion Include="Microsoft.FSharp.Compiler" Version="$(MicrosoftFSharpCompilerPackageVersion)" />
<PackageVersion Include="Microsoft.Net.Compilers.Toolset.Framework" Version="$(MicrosoftNetCompilersToolsetFrameworkPackageVersion)" />
<PackageVersion Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
<PackageVersion Include="Microsoft.Net.BuildServerUtils" Version="$(MicrosoftNetBuildServerUtilsVersion)" />
<PackageVersion Include="Microsoft.NET.HostModel" Version="$(MicrosoftNETHostModelVersion)" />
<PackageVersion Include="Microsoft.NET.Sdk.Razor.SourceGenerators.Transport" Version="$(MicrosoftNETSdkRazorSourceGeneratorsTransportPackageVersion)" />
<PackageVersion Include="Microsoft.NETCore.App.Runtime.win-x86" Version="$(MicrosoftNETCoreAppRuntimePackageVersion)" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
</PropertyGroup>
<PropertyGroup>
<!-- Dependencies from https://github.com/dotnet/roslyn -->
<MicrosoftNetBuildServerUtilsVersion>5.0.0-1.25359.101</MicrosoftNetBuildServerUtilsVersion>
<MicrosoftNetCompilersToolsetVersion>5.0.0-1.25359.101</MicrosoftNetCompilersToolsetVersion>
<MicrosoftNetCompilersToolsetFrameworkPackageVersion>5.0.0-1.25359.101</MicrosoftNetCompilersToolsetFrameworkPackageVersion>
<MicrosoftCodeAnalysisPackageVersion>5.0.0-1.25359.101</MicrosoftCodeAnalysisPackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Cli;
using Microsoft.Net.BuildServerUtils;

namespace Microsoft.DotNet.Cli.Utils;

Expand Down Expand Up @@ -70,6 +71,8 @@ public MSBuildForwardingAppWithoutLogging(MSBuildArgs msbuildArgs, string? msbui

EnvironmentVariable("MSBUILDUSESERVER", UseMSBuildServer ? "1" : "0");

EnvironmentVariable(BuildServerUtility.DotNetHostServerPath, GetHostServerPath());

// If DOTNET_CLI_RUN_MSBUILD_OUTOFPROC is set or we're asked to execute a non-default binary, call MSBuild out-of-proc.
if (AlwaysExecuteMSBuildOutOfProc || !string.Equals(MSBuildPath, defaultMSBuildPath, StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -135,6 +138,24 @@ public void EnvironmentVariable(string name, string? value)
}
}


public static string GetHostServerPath()
{
// If the path is set from outside, reuse it.
var hostServerPath = Env.GetEnvironmentVariable(BuildServerUtility.DotNetHostServerPath);
if (string.IsNullOrWhiteSpace(hostServerPath))
{
// Otherwise, construct a directory path under temp.
string baseDirectory = PathUtility.GetUserRestrictedTempDirectory();
hostServerPath = Path.Join(baseDirectory, "dotnet", "server", Product.TargetFrameworkVersion);
}

// Create the directory.
PathUtility.CreateUserRestrictedDirectory(hostServerPath);

return hostServerPath;
}

public int Execute()
{
if (_forwardingApp != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<PackageReference Include="NuGet.ProjectModel" />
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Microsoft.Net.BuildServerUtils" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="System.IO.Hashing" />
Expand Down
21 changes: 21 additions & 0 deletions src/Cli/Microsoft.DotNet.Cli.Utils/PathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ public static void EnsureDirectoryExists(string? directoryPath)
}
}

public static string GetUserRestrictedTempDirectory()
{
// We want a location where permissions are expected to be restricted to the current user.
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.GetTempPath()
: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
}

public static void CreateUserRestrictedDirectory(string path)
{
if (OperatingSystem.IsWindows())
{
Directory.CreateDirectory(path);
}
else
{
// NOTE: This modifies the permissions if needed, and throws if not possible.
Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
Copy link
Contributor

@KalleOlaviNiemitalo KalleOlaviNiemitalo Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment there looks misleading.

If the directory exists already, then CreateDirectory ignores the UnixFileMode, except throws if the argument contains undefined bits.

If the directory does not exist yet, then CreateDirectory passes the UnixFileMode to the mkdir system call, which uses it when creating the directory. But the umask of the process may cause mkdir not to grant all the permissions listed in the UnixFileMode. That would be the user's misconfiguration though; it is not reasonable to expect programs to work if the umask prevents permissions from being granted to the user.

Copy link
Member Author

@jjonescz jjonescz Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's indeed not what I've expected. But I think the behavior is acceptable, I will just update the comment.

}
}

public static bool TryDeleteDirectory(string directoryPath)
{
try
Expand Down
22 changes: 18 additions & 4 deletions src/Cli/Microsoft.DotNet.Cli.Utils/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@

namespace Microsoft.DotNet.Cli.Utils;

public class Product
public static class Product
{
public static string LongName => LocalizableStrings.DotNetSdkInfo;
public static readonly string Version = GetProductVersion();
public static readonly string Version;
public static readonly string TargetFrameworkVersion;

private static string GetProductVersion()
static Product()
{
DotnetVersionFile versionFile = DotnetFiles.VersionFileObject;
return versionFile.BuildNumber ??
Version = versionFile.BuildNumber ??
System.Diagnostics.FileVersionInfo.GetVersionInfo(
typeof(Product).GetTypeInfo().Assembly.Location)
.ProductVersion ??
string.Empty;

int firstDotIndex = Version.IndexOf('.');
if (firstDotIndex >= 0)
{
int secondDotIndex = Version.IndexOf('.', firstDotIndex + 1);
TargetFrameworkVersion = secondDotIndex >= 0
? Version.Substring(0, secondDotIndex)
: Version;
}
else
{
TargetFrameworkVersion = string.Empty;
}
}
}
5 changes: 5 additions & 0 deletions src/Cli/dotnet/BuildServer/BuildServerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ internal class BuildServerProvider(

public IEnumerable<IBuildServer> EnumerateBuildServers(ServerEnumerationFlags flags = ServerEnumerationFlags.All)
{
if ((flags & ServerEnumerationFlags.Unified) == ServerEnumerationFlags.Unified)
{
yield return new UnifiedBuildServer();
}

if ((flags & ServerEnumerationFlags.MSBuild) == ServerEnumerationFlags.MSBuild)
{
// Yield a single MSBuild server (handles server discovery itself)
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/dotnet/BuildServer/IBuildServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ internal interface IBuildServer

string Name { get; }

void Shutdown();
Task ShutdownAsync();
}
3 changes: 2 additions & 1 deletion src/Cli/dotnet/BuildServer/IBuildServerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ internal enum ServerEnumerationFlags
MSBuild = 1,
VBCSCompiler = 2,
Razor = 4,
All = MSBuild | VBCSCompiler | Razor
Unified = 5,
All = MSBuild | VBCSCompiler | Razor | Unified
}

internal interface IBuildServerProvider
Expand Down
3 changes: 2 additions & 1 deletion src/Cli/dotnet/BuildServer/MSBuildServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ internal class MSBuildServer : IBuildServer

public string Name => CliStrings.MSBuildServer;

public void Shutdown()
public Task ShutdownAsync()
{
BuildManager.DefaultBuildManager.ShutdownAllNodes();
return Task.CompletedTask;
}
}
7 changes: 5 additions & 2 deletions src/Cli/dotnet/BuildServer/RazorServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ internal class RazorServer(

public RazorPidFile PidFile { get; } = pidFile ?? throw new ArgumentNullException(nameof(pidFile));

public void Shutdown()
public Task ShutdownAsync()
{
if (!_fileSystem.File.Exists(PidFile.ServerPath.Value))
{
// The razor server path doesn't exist anymore so trying to shut it down would fail
// Ensure the pid file is cleaned up so we don't try to shut it down again
DeletePidFile();
return;

return Task.CompletedTask;
}

var command = _commandFactory
Expand Down Expand Up @@ -57,6 +58,8 @@ public void Shutdown()
// After a successful shutdown, ensure the pid file is deleted
// If the pid file was left behind due to a rude exit, this ensures we don't try to shut it down again
DeletePidFile();

return Task.CompletedTask;
}

void DeletePidFile()
Expand Down
33 changes: 33 additions & 0 deletions src/Cli/dotnet/BuildServer/UnifiedBuildServer.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 Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.Net.BuildServerUtils;

namespace Microsoft.DotNet.Cli.BuildServer;

internal sealed class UnifiedBuildServer : IBuildServer
{
public int ProcessId => 0; // Not used

public string Name => CliCommandStrings.UnifiedBuildServer;

public Task ShutdownAsync()
{
var hostServerPath = MSBuildForwardingAppWithoutLogging.GetHostServerPath();
Reporter.Output.WriteLine(CliCommandStrings.ShuttingDownUnifiedBuildServers, hostServerPath);

return BuildServerUtility.ShutdownServersAsync(
onProcessShutdownBegin: static (process) =>
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.ShuttingDownServerWithPid, process.ProcessName, process.Id).Red());
},
onError: static (error) =>
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.ShutDownFailed, CliCommandStrings.UnifiedBuildServer, error).Red());
},
hostServerPath: hostServerPath);
}
}
11 changes: 4 additions & 7 deletions src/Cli/dotnet/BuildServer/VBCSCompilerServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Microsoft.DotNet.Cli.BuildServer;

internal class VBCSCompilerServer(ICommandFactory commandFactory = null) : IBuildServer
internal class VBCSCompilerServer : IBuildServer
{
private static readonly string s_toolsetPackageName = "microsoft.net.sdk.compilers.toolset";
private static readonly string s_vbcsCompilerExeFileName = "VBCSCompiler.exe";
Expand All @@ -22,19 +22,14 @@ internal class VBCSCompilerServer(ICommandFactory commandFactory = null) : IBuil
"bincore",
"VBCSCompiler.dll");

private readonly ICommandFactory _commandFactory = commandFactory ?? new DotNetCommandFactory(alwaysRunOutOfProc: true);

public int ProcessId => 0; // Not yet used

public string Name => CliStrings.VBCSCompilerServer;

public void Shutdown()
public Task ShutdownAsync()
{
List<string> errors = null;

// Shutdown the compiler from the SDK.
execute(_commandFactory.Create("exec", [VBCSCompilerPath, s_shutdownArg]), ref errors);

// Shutdown toolset compilers.
Reporter.Verbose.WriteLine($"Shutting down '{s_toolsetPackageName}' compilers.");
var nuGetPackageRoot = SettingsUtility.GetGlobalPackagesFolder(Settings.LoadDefaultSettings(root: null));
Expand All @@ -61,6 +56,8 @@ public void Shutdown()
string.Join(Environment.NewLine, errors)));
}

return Task.CompletedTask;

static void execute(ICommand command, ref List<string> errors)
{
command = command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public BuildServerShutdownCommand(
bool msbuild = result.GetValue(BuildServerShutdownCommandParser.MSBuildOption);
bool vbcscompiler = result.GetValue(BuildServerShutdownCommandParser.VbcsOption);
bool razor = result.GetValue(BuildServerShutdownCommandParser.RazorOption);
bool all = !msbuild && !vbcscompiler && !razor;
bool unified = result.GetValue(BuildServerShutdownCommandParser.UnifiedOption);
bool all = !msbuild && !vbcscompiler && !razor && !unified;

_enumerationFlags = ServerEnumerationFlags.None;
if (msbuild || all)
Expand All @@ -46,6 +47,11 @@ public BuildServerShutdownCommand(
_enumerationFlags |= ServerEnumerationFlags.Razor;
}

if (unified || all)
{
_enumerationFlags |= ServerEnumerationFlags.Unified;
}

_serverProvider = serverProvider ?? new BuildServerProvider();
_useOrderedWait = useOrderedWait;
_reporter = reporter ?? Reporter.Output;
Expand Down Expand Up @@ -90,7 +96,7 @@ public override int Execute()
foreach (var server in _serverProvider.EnumerateBuildServers(_enumerationFlags))
{
WriteShutdownMessage(server);
tasks.Add((server, Task.Run(() => server.Shutdown())));
tasks.Add((server, Task.Run(() => server.ShutdownAsync())));
}

return tasks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ internal static class BuildServerShutdownCommandParser
Arity = ArgumentArity.Zero
};

public static readonly Option<bool> UnifiedOption = new("--unified")
{
Description = CliCommandStrings.UnifiedOptionDescription,
Arity = ArgumentArity.Zero
};

private static readonly Command Command = ConstructCommand();

public static Command GetCommand()
Expand All @@ -41,6 +47,7 @@ private static Command ConstructCommand()
command.Options.Add(MSBuildOption);
command.Options.Add(VbcsOption);
command.Options.Add(RazorOption);
command.Options.Add(UnifiedOption);

command.SetAction((parseResult) => new BuildServerShutdownCommand(parseResult).Execute());

Expand Down
9 changes: 9 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1863,6 +1863,15 @@ Your project targets multiple frameworks. Specify which framework to run using '
<data name="ShuttingDownServerWithPid" xml:space="preserve">
<value>Shutting down {0} (process {1})...</value>
</data>
<data name="ShuttingDownUnifiedBuildServers" xml:space="preserve">
<value>Shutting down unified build servers via named pipe ({0})...</value>
</data>
<data name="UnifiedBuildServer" xml:space="preserve">
<value>Unified build server</value>
</data>
<data name="UnifiedOptionDescription" xml:space="preserve">
<value>Shut down unified build servers via named pipes.</value>
</data>
<data name="SkipManifestUpdateOptionDescription" xml:space="preserve">
<value>Skip updating the workload manifests.</value>
</data>
Expand Down
21 changes: 5 additions & 16 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,7 @@ public static string GetArtifactsPath(string entryPointFileFullPath)
/// </summary>
public static string GetTempSubdirectory()
{
// We want a location where permissions are expected to be restricted to the current user.
string directory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.GetTempPath()
: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);

string directory = PathUtility.GetUserRestrictedTempDirectory();
return Path.Join(directory, "dotnet", "runfile");
}

Expand All @@ -614,17 +610,10 @@ public static string GetTempSubpath(string name)
/// </summary>
public static void CreateTempSubdirectory(string path)
{
if (OperatingSystem.IsWindows())
{
Directory.CreateDirectory(path);
}
else
{
// Ensure only the current user has access to the directory to avoid leaking the program to other users.
// We don't mind that permissions might be different if the directory already exists,
// since it's under user's local directory and its path should be unique.
Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
}
// Ensure only the current user has access to the directory to avoid leaking the program to other users.
// We don't mind that permissions might be different if the directory already exists,
// since it's under user's local directory and its path should be unique.
PathUtility.CreateUserRestrictedDirectory(path);
}

public static void WriteProjectFile(
Expand Down
Loading