@@ -38,41 +38,12 @@ public static void EfficientCopyTo(this Stream input, Stream output)
3838
3939 public static int Read ( this Stream stream , byte [ ] buffer , int offset , int count , TimeSpan timeout , CancellationToken cancellationToken )
4040 {
41- try
42- {
43- using var manualResetEvent = new ManualResetEventSlim ( ) ;
44- var readOperation = stream . BeginRead (
45- buffer ,
46- offset ,
47- count ,
48- state => ( ( ManualResetEventSlim ) state . AsyncState ) . Set ( ) ,
49- manualResetEvent ) ;
50-
51- if ( readOperation . IsCompleted || manualResetEvent . Wait ( timeout , cancellationToken ) )
52- {
53- return stream . EndRead ( readOperation ) ;
54- }
55- }
56- catch ( OperationCanceledException )
57- {
58- // Have to suppress OperationCanceledException here, it will be thrown after the stream will be disposed.
59- }
60- catch ( ObjectDisposedException )
61- {
62- throw new IOException ( ) ;
63- }
64-
65- try
66- {
67- stream . Dispose ( ) ;
68- }
69- catch
70- {
71- // Ignore any exceptions
72- }
73-
74- cancellationToken . ThrowIfCancellationRequested ( ) ;
75- throw new TimeoutException ( ) ;
41+ return UseStreamWithTimeout (
42+ stream ,
43+ ( str , state ) => str . Read ( state . buffer , state . offset , state . count ) ,
44+ ( buffer , offset , count ) ,
45+ timeout ,
46+ cancellationToken ) ;
7647 }
7748
7849 public static async Task < int > ReadAsync ( this Stream stream , byte [ ] buffer , int offset , int count , TimeSpan timeout , CancellationToken cancellationToken )
@@ -219,43 +190,16 @@ public static async Task ReadBytesAsync(this Stream stream, byte[] destination,
219190
220191 public static void Write ( this Stream stream , byte [ ] buffer , int offset , int count , TimeSpan timeout , CancellationToken cancellationToken )
221192 {
222- try
223- {
224- using var manualResetEvent = new ManualResetEventSlim ( ) ;
225- var writeOperation = stream . BeginWrite (
226- buffer ,
227- offset ,
228- count ,
229- state => ( ( ManualResetEventSlim ) state . AsyncState ) . Set ( ) ,
230- manualResetEvent ) ;
231-
232- if ( writeOperation . IsCompleted || manualResetEvent . Wait ( timeout , cancellationToken ) )
193+ UseStreamWithTimeout (
194+ stream ,
195+ ( str , state ) =>
233196 {
234- stream . EndWrite ( writeOperation ) ;
235- return ;
236- }
237- }
238- catch ( OperationCanceledException )
239- {
240- // Have to suppress OperationCanceledException here, it will be thrown after the stream will be disposed.
241- }
242- catch ( ObjectDisposedException )
243- {
244- // It's possible to get ObjectDisposedException when the connection pool was closed with interruptInUseConnections set to true.
245- throw new IOException ( ) ;
246- }
247-
248- try
249- {
250- stream . Dispose ( ) ;
251- }
252- catch
253- {
254- // Ignore any exceptions
255- }
256-
257- cancellationToken . ThrowIfCancellationRequested ( ) ;
258- throw new TimeoutException ( ) ;
197+ str . Write ( state . buffer , state . offset , state . count ) ;
198+ return true ;
199+ } ,
200+ ( buffer , offset , count ) ,
201+ timeout ,
202+ cancellationToken ) ;
259203 }
260204
261205 public static async Task WriteAsync ( this Stream stream , byte [ ] buffer , int offset , int count , TimeSpan timeout , CancellationToken cancellationToken )
@@ -325,5 +269,89 @@ public static async Task WriteBytesAsync(this Stream stream, OperationContext op
325269 count -= bytesToWrite ;
326270 }
327271 }
272+
273+ private static TResult UseStreamWithTimeout < TResult , TState > ( Stream stream , Func < Stream , TState , TResult > method , TState state , TimeSpan timeout , CancellationToken cancellationToken )
274+ {
275+ StreamDisposeCallbackState callbackState = null ;
276+ Timer timer = null ;
277+ CancellationTokenRegistration cancellationSubscription = default ;
278+ if ( timeout != Timeout . InfiniteTimeSpan )
279+ {
280+ callbackState = new StreamDisposeCallbackState ( stream ) ;
281+ timer = new Timer ( DisposeStreamCallback , callbackState , timeout , Timeout . InfiniteTimeSpan ) ;
282+ }
283+
284+ if ( cancellationToken . CanBeCanceled )
285+ {
286+ callbackState ??= new StreamDisposeCallbackState ( stream ) ;
287+ cancellationSubscription = cancellationToken . Register ( DisposeStreamCallback , callbackState ) ;
288+ }
289+
290+ try
291+ {
292+ var result = method ( stream , state ) ;
293+ if ( callbackState ? . TryChangeState ( OperationState . Done ) == false )
294+ {
295+ // if cannot change the state - then the stream was/will be disposed, throw here
296+ throw new IOException ( ) ;
297+ }
298+
299+ return result ;
300+ }
301+ catch ( IOException )
302+ {
303+ if ( callbackState ? . OperationState == OperationState . Cancelled )
304+ {
305+ cancellationToken . ThrowIfCancellationRequested ( ) ;
306+ throw new TimeoutException ( ) ;
307+ }
308+
309+ throw ;
310+ }
311+ finally
312+ {
313+ timer ? . Dispose ( ) ;
314+ cancellationSubscription . Dispose ( ) ;
315+ }
316+
317+ static void DisposeStreamCallback ( object state )
318+ {
319+ var disposeCallbackState = ( StreamDisposeCallbackState ) state ;
320+ if ( ! disposeCallbackState . TryChangeState ( OperationState . Cancelled ) )
321+ {
322+ // if cannot change the state - then I/O was already succeeded
323+ return ;
324+ }
325+
326+ try
327+ {
328+ disposeCallbackState . Stream . Dispose ( ) ;
329+ }
330+ catch ( Exception )
331+ {
332+ // callbacks should not fail, suppress any exceptions here
333+ }
334+ }
335+ }
336+
337+ private record StreamDisposeCallbackState ( Stream Stream )
338+ {
339+ private int _operationState = 0 ;
340+
341+ public OperationState OperationState
342+ {
343+ get => ( OperationState ) _operationState ;
344+ }
345+
346+ public bool TryChangeState ( OperationState newState ) =>
347+ Interlocked . CompareExchange ( ref _operationState , ( int ) newState , ( int ) OperationState . InProgress ) == ( int ) OperationState . InProgress ;
348+ }
349+
350+ private enum OperationState
351+ {
352+ InProgress = 0 ,
353+ Done ,
354+ Cancelled ,
355+ }
328356 }
329357}
0 commit comments