1
+ // Copyright (c) Alexandre Mutel. All rights reserved.
2
+ // Licensed under the BSD-Clause 2 license.
3
+ // See license.txt file in the project root for full license information.
4
+
5
+ using System . Diagnostics ;
6
+ using System . Diagnostics . Tracing ;
7
+ using System . Net . Sockets ;
8
+ using Microsoft . Diagnostics . NETCore . Client ;
9
+ using Microsoft . Diagnostics . Tracing . Parsers ;
10
+ using Ultra . Sampler ;
11
+
12
+ namespace Ultra . Core ;
13
+
14
+ /// <summary>
15
+ /// Handles an EventPipe session to a diagnostic port.
16
+ /// </summary>
17
+ internal class DiagnosticPortSession
18
+ {
19
+ private readonly int _pid ;
20
+ private readonly bool _sampler ;
21
+ private readonly string _baseName ;
22
+ private readonly Task _connectTask ;
23
+ private readonly object _sessionLock = new ( ) ;
24
+ private Task ? _profilingTask ;
25
+ private readonly CancellationTokenSource _cancelConnectSource ;
26
+ private DiagnosticsClient ? _diagnosticsClient ;
27
+ private EventPipeSession ? _eventPipeSession ;
28
+ private string ? _nettraceFilePath ;
29
+ private FileStream ? _nettraceFileStream ;
30
+ private Task ? _eventStreamCopyTask ;
31
+ private bool _disposed ;
32
+
33
+ public DiagnosticPortSession ( int pid , bool sampler , string baseName , CancellationToken token )
34
+ {
35
+ _pid = pid ;
36
+ _sampler = sampler ;
37
+ _baseName = baseName ;
38
+ _cancelConnectSource = new CancellationTokenSource ( ) ;
39
+ _connectTask = ConnectAndStartProfilingImpl ( pid , sampler , baseName , token ) ;
40
+ }
41
+
42
+ private async Task ConnectAndStartProfilingImpl ( int pid , bool sampler , string baseName , CancellationToken token )
43
+ {
44
+ CancellationTokenSource linkedCancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( token , _cancelConnectSource . Token ) ;
45
+ try
46
+ {
47
+
48
+ var connectCancellationToken = linkedCancellationTokenSource . Token ;
49
+
50
+ if ( sampler )
51
+ {
52
+ _cancelConnectSource . CancelAfter ( 500 ) ;
53
+ }
54
+
55
+ var connectionAddress = await TryFindConnectionAddress ( pid , sampler , connectCancellationToken ) . ConfigureAwait ( false ) ;
56
+ if ( connectionAddress is null ) return ;
57
+
58
+ _diagnosticsClient = ( await DiagnosticsClientConnector . FromDiagnosticPort ( connectionAddress , connectCancellationToken ) . ConfigureAwait ( false ) ) ? . Instance ;
59
+ if ( _diagnosticsClient is null ) return ;
60
+
61
+ await _diagnosticsClient . WaitForConnectionAsync ( connectCancellationToken ) . ConfigureAwait ( false ) ;
62
+ }
63
+ catch ( OperationCanceledException ex )
64
+ {
65
+ if ( sampler && _cancelConnectSource is not null && _cancelConnectSource . IsCancellationRequested )
66
+ {
67
+ throw new InvalidOperationException ( $ "Cannot connect to the diagnostic port socket for pid { pid } ", ex ) ;
68
+ }
69
+ return ;
70
+ }
71
+ finally
72
+ {
73
+ linkedCancellationTokenSource . Dispose ( ) ;
74
+ }
75
+ }
76
+
77
+ public void StartProfiling ( CancellationToken token )
78
+ {
79
+ // We want to make sure that we are not disposing while we are connecting
80
+ Monitor . Enter ( _sessionLock ) ;
81
+ try
82
+ {
83
+ if ( _disposed )
84
+ {
85
+ return ;
86
+ }
87
+
88
+ _profilingTask = _connectTask . ContinueWith ( async task =>
89
+ {
90
+
91
+ _nettraceFilePath = Path . Combine ( Environment . CurrentDirectory , $ "{ _baseName } _{ ( _sampler ? "sampler" : "main" ) } _{ _pid } .nettrace") ;
92
+ _nettraceFileStream = new FileStream ( _nettraceFilePath , FileMode . Create , FileAccess . Write , FileShare . Read , 65536 , FileOptions . Asynchronous ) ;
93
+
94
+ long keywords = - 1 ;
95
+ var providerName = UltraSamplerParser . Name ;
96
+ var level = EventLevel . Verbose ;
97
+
98
+ if ( ! _sampler )
99
+ {
100
+ providerName = "Microsoft-Windows-DotNETRuntime" ;
101
+ keywords = ( long ) (
102
+ ClrTraceEventParser . Keywords . JITSymbols |
103
+ ClrTraceEventParser . Keywords . Exception |
104
+ ClrTraceEventParser . Keywords . GC |
105
+ ClrTraceEventParser . Keywords . GCHeapAndTypeNames |
106
+ ClrTraceEventParser . Keywords . Interop |
107
+ ClrTraceEventParser . Keywords . JITSymbols |
108
+ ClrTraceEventParser . Keywords . Jit |
109
+ ClrTraceEventParser . Keywords . JittedMethodILToNativeMap |
110
+ ClrTraceEventParser . Keywords . Loader |
111
+ ClrTraceEventParser . Keywords . Stack |
112
+ ClrTraceEventParser . Keywords . StartEnumeration
113
+ ) ;
114
+ }
115
+
116
+ var ultraEventProvider = new EventPipeProvider ( providerName , level , ( long ) keywords ) ;
117
+ var config = new EventPipeSessionConfiguration ( [ ultraEventProvider ] , 512 , ! _sampler , true ) ;
118
+ _eventPipeSession = await _diagnosticsClient ! . StartEventPipeSessionAsync ( config , token ) . ConfigureAwait ( false ) ;
119
+ _eventStreamCopyTask = _eventPipeSession . EventStream . CopyToAsync ( _nettraceFileStream , token ) ;
120
+ } , token ) ;
121
+ }
122
+ finally
123
+ {
124
+ Monitor . Exit ( _sessionLock ) ;
125
+ }
126
+ }
127
+
128
+ public long GetNettraceFileLength ( ) => _nettraceFileStream ? . Length ?? 0 ;
129
+
130
+ public async Task WaitForConnectAndStartSession ( )
131
+ {
132
+ await _connectTask . ConfigureAwait ( false ) ;
133
+ }
134
+
135
+ private static async Task < string ? > TryFindConnectionAddress ( int pid , bool sampler , CancellationToken token )
136
+ {
137
+ var tempFolder = Path . GetTempPath ( ) ;
138
+ tempFolder = sampler ? Path . Combine ( tempFolder , ".ultra" ) : tempFolder ;
139
+
140
+ var pattern = $ "dotnet-diagnostic-{ pid } -*-socket";
141
+ string ? diagnosticPortSocket = null ;
142
+
143
+ while ( true )
144
+ {
145
+ if ( Directory . Exists ( tempFolder ) )
146
+ {
147
+ DateTime lastWriteTime = default ;
148
+ foreach ( var file in Directory . EnumerateFiles ( tempFolder , pattern ) )
149
+ {
150
+ var fileInfo = new FileInfo ( file ) ;
151
+ if ( fileInfo . LastWriteTime > lastWriteTime )
152
+ {
153
+ diagnosticPortSocket = file ;
154
+ lastWriteTime = fileInfo . LastWriteTime ;
155
+ }
156
+ }
157
+
158
+ if ( diagnosticPortSocket != null )
159
+ {
160
+ // Force connect mode
161
+ diagnosticPortSocket = $ "{ diagnosticPortSocket } ,connect";
162
+ break ;
163
+ }
164
+ }
165
+
166
+ await Task . Delay ( 10 , token ) . ConfigureAwait ( false ) ;
167
+ }
168
+
169
+ return diagnosticPortSocket ;
170
+ }
171
+
172
+ public async ValueTask StopAndDisposeAsync ( )
173
+ {
174
+ Monitor . Enter ( _sessionLock ) ;
175
+ try
176
+ {
177
+ if ( _profilingTask is null )
178
+ {
179
+ // We cancel any pending connection
180
+ await _cancelConnectSource . CancelAsync ( ) ;
181
+ }
182
+ else
183
+ {
184
+ try
185
+ {
186
+ // We wait for the session to start (we will close it right after below
187
+ await _profilingTask . ConfigureAwait ( false ) ;
188
+ }
189
+ catch
190
+ {
191
+ // Ignore
192
+ }
193
+ }
194
+
195
+ Debug . Assert ( _eventStreamCopyTask is not null ) ;
196
+ try
197
+ {
198
+ await _eventStreamCopyTask . ConfigureAwait ( false ) ;
199
+ }
200
+ catch
201
+ {
202
+ // Ignore
203
+ }
204
+
205
+ Debug . Assert ( _nettraceFileStream is not null ) ;
206
+ try
207
+ {
208
+ await _nettraceFileStream . DisposeAsync ( ) . ConfigureAwait ( false ) ;
209
+ }
210
+ catch
211
+ {
212
+ // Ignore
213
+ }
214
+
215
+ Debug . Assert ( _eventPipeSession is not null ) ;
216
+ try
217
+ {
218
+ await _eventPipeSession . StopAsync ( CancellationToken . None ) . ConfigureAwait ( false ) ;
219
+ }
220
+ catch
221
+ {
222
+ // Ignore
223
+ }
224
+ finally
225
+ {
226
+ try
227
+ {
228
+ _eventPipeSession . Dispose ( ) ;
229
+ }
230
+ catch
231
+ {
232
+ // Ignore
233
+ }
234
+ }
235
+ }
236
+ finally
237
+ {
238
+ _disposed = true ;
239
+ Monitor . Exit ( _sessionLock ) ;
240
+
241
+ _cancelConnectSource . Dispose ( ) ;
242
+ }
243
+ }
244
+
245
+ private async Task StopAsync ( CancellationToken token )
246
+ {
247
+ if ( _eventPipeSession is null ) return ;
248
+
249
+ try
250
+ {
251
+ await _eventPipeSession . StopAsync ( token ) . ConfigureAwait ( false ) ;
252
+ }
253
+ catch ( EndOfStreamException )
254
+ {
255
+
256
+ }
257
+ catch ( TimeoutException )
258
+ {
259
+
260
+ }
261
+ catch ( OperationCanceledException )
262
+ {
263
+
264
+ }
265
+ catch ( PlatformNotSupportedException )
266
+ {
267
+
268
+ }
269
+ catch ( ServerNotAvailableException )
270
+ {
271
+
272
+ }
273
+ catch ( SocketException )
274
+ {
275
+
276
+ }
277
+ }
278
+ }
0 commit comments