-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Client.cs
196 lines (165 loc) · 7.34 KB
/
Client.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
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}.");
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();
}
}
}
}
}