|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.IO.Pipes;
#if NETFRAMEWORK
using System.Security.AccessControl;
using System.Security.Principal;
#endif
namespace Microsoft.NET.Sdk.Razor.Tool
{
internal abstract class Client : IDisposable
{
private static int counter;
// From https://github.com/dotnet/corefx/blob/29cd6a0b0ac2993cee23ebaf36ca3d4bce6dd75f/src/System.IO.Pipes/ref/System.IO.Pipes.cs#L93.
// Using the enum value directly as this option is not available in netstandard.
private const PipeOptions PipeOptionCurrentUserOnly = (PipeOptions)536870912;
private static readonly PipeOptions _pipeOptions = GetPipeOptions();
public abstract Stream Stream { get; }
public abstract string Identifier { get; }
public void Dispose()
{
Dispose(disposing: true);
}
public abstract Task WaitForDisconnectAsync(CancellationToken cancellationToken);
protected virtual void Dispose(bool disposing)
{
}
// Based on: https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Shared/BuildServerConnection.cs#L290
public static async Task<Client> ConnectAsync(string pipeName, TimeSpan? timeout, CancellationToken cancellationToken)
{
var timeoutMilliseconds = timeout == null ? Timeout.Infinite : (int)timeout.Value.TotalMilliseconds;
try
{
// Machine-local named pipes are named "\\.\pipe\<pipename>".
// We use the SHA1 of the directory the compiler exes live in as the pipe name.
// The NamedPipeClientStream class handles the "\\.\pipe\" part for us.
ServerLogger.Log("Attempt to open named pipe '{0}'", pipeName);
var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, _pipeOptions);
cancellationToken.ThrowIfCancellationRequested();
ServerLogger.Log("Attempt to connect named pipe '{0}'", pipeName);
try
{
await stream.ConnectAsync(timeoutMilliseconds, cancellationToken);
}
catch (Exception e) when (e is IOException || e is TimeoutException)
{
// Note: IOException can also indicate timeout.
// From docs:
// - TimeoutException: Could not connect to the server within the specified timeout period.
// - IOException: The server is connected to another client and the time-out period has expired.
ServerLogger.Log($"Connecting to server timed out after {timeoutMilliseconds} ms");
return null;
}
ServerLogger.Log("Named pipe '{0}' connected", pipeName);
cancellationToken.ThrowIfCancellationRequested();
#if NETFRAMEWORK
// Verify that we own the pipe.
if (!CheckPipeConnectionOwnership(stream))
{
ServerLogger.Log("Owner of named pipe is incorrect");
return null;
}
#endif
return new NamedPipeClient(stream, GetNextIdentifier());
}
catch (Exception e) when (!(e is TaskCanceledException || e is OperationCanceledException))
{
ServerLogger.LogException(e, "Exception while connecting to process");
return null;
}
}
#if NETFRAMEWORK
/// <summary>
/// Check to ensure that the named pipe server we connected to is owned by the same
/// user.
/// </summary>
private static bool CheckPipeConnectionOwnership(NamedPipeClientStream pipeStream)
{
try
{
if (PlatformInformation.IsWindows)
{
using (var currentIdentity = WindowsIdentity.GetCurrent())
{
var currentOwner = currentIdentity.Owner;
var remotePipeSecurity = GetPipeSecurity(pipeStream);
var remoteOwner = remotePipeSecurity.GetOwner(typeof(SecurityIdentifier));
return currentOwner.Equals(remoteOwner);
}
}
// We don't need to verify on non-windows as that will be taken care of by the
// PipeOptions.CurrentUserOnly flag.
return false;
}
catch (Exception ex)
{
ServerLogger.LogException(ex, "Checking pipe connection");
return false;
}
}
private static ObjectSecurity GetPipeSecurity(PipeStream pipeStream)
{
return pipeStream.GetAccessControl();
}
#endif
private static PipeOptions GetPipeOptions()
{
var options = PipeOptions.Asynchronous;
if (Enum.IsDefined(typeof(PipeOptions), PipeOptionCurrentUserOnly))
{
return options | PipeOptionCurrentUserOnly;
}
return options;
}
private static string GetNextIdentifier()
{
var id = Interlocked.Increment(ref counter);
return "clientconnection-" + id;
}
private class NamedPipeClient : Client
{
public NamedPipeClient(NamedPipeClientStream stream, string identifier)
{
Stream = stream;
Identifier = identifier;
}
public override Stream Stream { get; }
public override string Identifier { get; }
public override async Task WaitForDisconnectAsync(CancellationToken cancellationToken)
{
if (!(Stream is PipeStream pipeStream))
{
return;
}
// We have to poll for disconnection by reading, PipeStream.IsConnected isn't reliable unless you
// actually do a read - which will cause it to update its state.
while (!cancellationToken.IsCancellationRequested && pipeStream.IsConnected)
{
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
try
{
ServerLogger.Log($"Before poking pipe {Identifier}.");
// Stream.ReadExactlyAsync with a zero-length buffer will not update the state of the pipe.
// We need to trigger a state update without actually reading the data.
_ = await Stream.ReadAsync(Array.Empty<byte>(), 0, 0, cancellationToken);
ServerLogger.Log($"After poking pipe {Identifier}.");
}
catch (OperationCanceledException)
{
}
catch (Exception e)
{
// It is okay for this call to fail. Errors will be reflected in the
// IsConnected property which will be read on the next iteration.
ServerLogger.LogException(e, $"Error poking pipe {Identifier}.");
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Stream.Dispose();
}
}
}
}
}
|