-
Notifications
You must be signed in to change notification settings - Fork 4.7k
/
FileStream.Windows.cs
1618 lines (1433 loc) · 75.5 KB
/
FileStream.Windows.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using System.Runtime.CompilerServices;
/*
* Win32FileStream supports different modes of accessing the disk - async mode
* and sync mode. They are two completely different codepaths in the
* sync & async methods (i.e. Read/Write vs. ReadAsync/WriteAsync). File
* handles in NT can be opened in only sync or overlapped (async) mode,
* and we have to deal with this pain. Stream has implementations of
* the sync methods in terms of the async ones, so we'll
* call through to our base class to get those methods when necessary.
*
* Also buffering is added into Win32FileStream as well. Folded in the
* code from BufferedStream, so all the comments about it being mostly
* aggressive (and the possible perf improvement) apply to Win32FileStream as
* well. Also added some buffering to the async code paths.
*
* Class Invariants:
* The class has one buffer, shared for reading & writing. It can only be
* used for one or the other at any point in time - not both. The following
* should be true:
* 0 <= _readPos <= _readLen < _bufferSize
* 0 <= _writePos < _bufferSize
* _readPos == _readLen && _readPos > 0 implies the read buffer is valid,
* but we're at the end of the buffer.
* _readPos == _readLen == 0 means the read buffer contains garbage.
* Either _writePos can be greater than 0, or _readLen & _readPos can be
* greater than zero, but neither can be greater than zero at the same time.
*
*/
namespace System.IO
{
public partial class FileStream : Stream
{
private bool _canSeek;
private bool _isPipe; // Whether to disable async buffering code.
private long _appendStart; // When appending, prevent overwriting file.
private static readonly unsafe IOCompletionCallback s_ioCallback = FileStreamCompletionSource.IOCallback;
private Task _activeBufferOperation = Task.CompletedTask; // tracks in-progress async ops using the buffer
private PreAllocatedOverlapped? _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations
private FileStreamCompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped
private void Init(FileMode mode, FileShare share, string originalPath)
{
if (!PathInternal.IsExtended(originalPath))
{
// To help avoid stumbling into opening COM/LPT ports by accident, we will block on non file handles unless
// we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into
// \\.\CON, so we'll only allow the \\?\ syntax.
int fileType = Interop.Kernel32.GetFileType(_fileHandle);
if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK)
{
int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN
? Marshal.GetLastWin32Error()
: Interop.Errors.ERROR_SUCCESS;
_fileHandle.Dispose();
if (errorCode != Interop.Errors.ERROR_SUCCESS)
{
throw Win32Marshal.GetExceptionForWin32Error(errorCode);
}
throw new NotSupportedException(SR.NotSupported_FileStreamOnNonFiles);
}
}
// This is necessary for async IO using IO Completion ports via our
// managed Threadpool API's. This (theoretically) calls the OS's
// BindIoCompletionCallback method, and passes in a stub for the
// LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped
// struct for this request and gets a delegate to a managed callback
// from there, which it then calls on a threadpool thread. (We allocate
// our native OVERLAPPED structs 2 pointers too large and store EE state
// & GC handles there, one to an IAsyncResult, the other to a delegate.)
if (_useAsyncIO)
{
try
{
_fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle);
}
catch (ArgumentException ex)
{
throw new IOException(SR.IO_BindHandleFailed, ex);
}
finally
{
if (_fileHandle.ThreadPoolBinding == null)
{
// We should close the handle so that the handle is not open until SafeFileHandle GC
Debug.Assert(!_exposedHandle, "Are we closing handle that we exposed/not own, how?");
_fileHandle.Dispose();
}
}
}
_canSeek = true;
// For Append mode...
if (mode == FileMode.Append)
{
_appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End);
}
else
{
_appendStart = -1;
}
}
private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAsyncIO)
{
#if DEBUG
bool hadBinding = handle.ThreadPoolBinding != null;
try
{
#endif
InitFromHandleImpl(handle, useAsyncIO);
#if DEBUG
}
catch
{
Debug.Assert(hadBinding || handle.ThreadPoolBinding == null, "We should never error out with a ThreadPoolBinding we've added");
throw;
}
#endif
}
private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO)
{
int handleType = Interop.Kernel32.GetFileType(handle);
Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, "FileStream was passed an unknown file type!");
_canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK;
_isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE;
// This is necessary for async IO using IO Completion ports via our
// managed Threadpool API's. This calls the OS's
// BindIoCompletionCallback method, and passes in a stub for the
// LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped
// struct for this request and gets a delegate to a managed callback
// from there, which it then calls on a threadpool thread. (We allocate
// our native OVERLAPPED structs 2 pointers too large and store EE
// state & a handle to a delegate there.)
//
// If, however, we've already bound this file handle to our completion port,
// don't try to bind it again because it will fail. A handle can only be
// bound to a single completion port at a time.
if (useAsyncIO && !(handle.IsAsync ?? false))
{
try
{
handle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(handle);
}
catch (Exception ex)
{
// If you passed in a synchronous handle and told us to use
// it asynchronously, throw here.
throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle), ex);
}
}
else if (!useAsyncIO)
{
VerifyHandleIsSync(handle);
}
if (_canSeek)
SeekCore(handle, 0, SeekOrigin.Current);
else
_filePosition = 0;
}
private static unsafe Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(FileShare share)
{
Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default;
if ((share & FileShare.Inheritable) != 0)
{
secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES
{
nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES),
bInheritHandle = Interop.BOOL.TRUE
};
}
return secAttrs;
}
private bool HasActiveBufferOperation => !_activeBufferOperation.IsCompleted;
public override bool CanSeek => _canSeek;
private unsafe long GetLengthInternal()
{
Interop.Kernel32.FILE_STANDARD_INFO info;
if (!Interop.Kernel32.GetFileInformationByHandleEx(_fileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO)))
throw Win32Marshal.GetExceptionForLastWin32Error(_path);
long len = info.EndOfFile;
// If we're writing near the end of the file, we must include our
// internal buffer in our Length calculation. Don't flush because
// we use the length of the file in our async write method.
if (_writePos > 0 && _filePosition + _writePos > len)
len = _writePos + _filePosition;
return len;
}
protected override void Dispose(bool disposing)
{
// Nothing will be done differently based on whether we are
// disposing vs. finalizing. This is taking advantage of the
// weak ordering between normal finalizable objects & critical
// finalizable objects, which I included in the SafeHandle
// design for Win32FileStream, which would often "just work" when
// finalized.
try
{
if (_fileHandle != null && !_fileHandle.IsClosed && _writePos > 0)
{
// Flush data to disk iff we were writing. After
// thinking about this, we also don't need to flush
// our read position, regardless of whether the handle
// was exposed to the user. They probably would NOT
// want us to do this.
try
{
FlushWriteBuffer(!disposing);
}
catch (Exception e) when (IsIoRelatedException(e) && !disposing)
{
// On finalization, ignore failures from trying to flush the write buffer,
// e.g. if this stream is wrapping a pipe and the pipe is now broken.
}
}
}
finally
{
if (_fileHandle != null && !_fileHandle.IsClosed)
{
_fileHandle.ThreadPoolBinding?.Dispose();
_fileHandle.Dispose();
}
_preallocatedOverlapped?.Dispose();
_canSeek = false;
// Don't set the buffer to null, to avoid a NullReferenceException
// when users have a race condition in their code (i.e. they call
// Close when calling another method on Stream like Read).
}
}
public override ValueTask DisposeAsync() =>
GetType() == typeof(FileStream) ?
DisposeAsyncCore() :
base.DisposeAsync();
private async ValueTask DisposeAsyncCore()
{
// Same logic as in Dispose(), except with async counterparts.
// TODO: https://github.com/dotnet/runtime/issues/27643: FlushAsync does synchronous work.
try
{
if (_fileHandle != null && !_fileHandle.IsClosed && _writePos > 0)
{
await FlushAsyncInternal(default).ConfigureAwait(false);
}
}
finally
{
if (_fileHandle != null && !_fileHandle.IsClosed)
{
_fileHandle.ThreadPoolBinding?.Dispose();
_fileHandle.Dispose();
}
_preallocatedOverlapped?.Dispose();
_canSeek = false;
GC.SuppressFinalize(this); // the handle is closed; nothing further for the finalizer to do
}
}
private void FlushOSBuffer()
{
if (!Interop.Kernel32.FlushFileBuffers(_fileHandle))
{
throw Win32Marshal.GetExceptionForLastWin32Error(_path);
}
}
// Returns a task that flushes the internal write buffer
private Task FlushWriteAsync(CancellationToken cancellationToken)
{
Debug.Assert(_useAsyncIO);
Debug.Assert(_readPos == 0 && _readLength == 0, "FileStream: Read buffer must be empty in FlushWriteAsync!");
// If the buffer is already flushed, don't spin up the OS write
if (_writePos == 0) return Task.CompletedTask;
Task flushTask = WriteAsyncInternalCore(new ReadOnlyMemory<byte>(GetBuffer(), 0, _writePos), cancellationToken);
_writePos = 0;
// Update the active buffer operation
_activeBufferOperation = HasActiveBufferOperation ?
Task.WhenAll(_activeBufferOperation, flushTask) :
flushTask;
return flushTask;
}
private void FlushWriteBufferForWriteByte() => FlushWriteBuffer();
// Writes are buffered. Anytime the buffer fills up
// (_writePos + delta > _bufferSize) or the buffer switches to reading
// and there is left over data (_writePos > 0), this function must be called.
private void FlushWriteBuffer(bool calledFromFinalizer = false)
{
if (_writePos == 0) return;
Debug.Assert(_readPos == 0 && _readLength == 0, "FileStream: Read buffer must be empty in FlushWrite!");
if (_useAsyncIO)
{
Task writeTask = FlushWriteAsync(CancellationToken.None);
// With our Whidbey async IO & overlapped support for AD unloads,
// we don't strictly need to block here to release resources
// since that support takes care of the pinning & freeing the
// overlapped struct. We need to do this when called from
// Close so that the handle is closed when Close returns, but
// we don't need to call EndWrite from the finalizer.
// Additionally, if we do call EndWrite, we block forever
// because AD unloads prevent us from running the managed
// callback from the IO completion port. Blocking here when
// called from the finalizer during AD unload is clearly wrong,
// but we can't use any sort of test for whether the AD is
// unloading because if we weren't unloading, an AD unload
// could happen on a separate thread before we call EndWrite.
if (!calledFromFinalizer)
{
writeTask.GetAwaiter().GetResult();
}
}
else
{
WriteCore(new ReadOnlySpan<byte>(GetBuffer(), 0, _writePos));
}
_writePos = 0;
}
private void SetLengthInternal(long value)
{
// Handle buffering updates.
if (_writePos > 0)
{
FlushWriteBuffer();
}
else if (_readPos < _readLength)
{
FlushReadBuffer();
}
_readPos = 0;
_readLength = 0;
if (_appendStart != -1 && value < _appendStart)
throw new IOException(SR.IO_SetLengthAppendTruncate);
SetLengthCore(value);
}
// We absolutely need this method broken out so that WriteInternalCoreAsync can call
// a method without having to go through buffering code that might call FlushWrite.
private void SetLengthCore(long value)
{
Debug.Assert(value >= 0, "value >= 0");
long origPos = _filePosition;
VerifyOSHandlePosition();
if (_filePosition != value)
SeekCore(_fileHandle, value, SeekOrigin.Begin);
if (!Interop.Kernel32.SetEndOfFile(_fileHandle))
{
int errorCode = Marshal.GetLastWin32Error();
if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER)
throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_FileLengthTooBig);
throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path);
}
// Return file pointer to where it was before setting length
if (origPos != value)
{
if (origPos < value)
SeekCore(_fileHandle, origPos, SeekOrigin.Begin);
else
SeekCore(_fileHandle, 0, SeekOrigin.End);
}
}
// Instance method to help code external to this MarshalByRefObject avoid
// accessing its fields by ref. This avoids a compiler warning.
private FileStreamCompletionSource? CompareExchangeCurrentOverlappedOwner(FileStreamCompletionSource? newSource, FileStreamCompletionSource? existingSource) =>
Interlocked.CompareExchange(ref _currentOverlappedOwner, newSource, existingSource);
private int ReadSpan(Span<byte> destination)
{
Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode");
Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength),
"We're either reading or writing, but not both.");
bool isBlocked = false;
int n = _readLength - _readPos;
// if the read buffer is empty, read into either user's array or our
// buffer, depending on number of bytes user asked for and buffer size.
if (n == 0)
{
if (!CanRead) throw Error.GetReadNotSupported();
if (_writePos > 0) FlushWriteBuffer();
if (!CanSeek || (destination.Length >= _bufferLength))
{
n = ReadNative(destination);
// Throw away read buffer.
_readPos = 0;
_readLength = 0;
return n;
}
n = ReadNative(GetBuffer());
if (n == 0) return 0;
isBlocked = n < _bufferLength;
_readPos = 0;
_readLength = n;
}
// Now copy min of count or numBytesAvailable (i.e. near EOF) to array.
if (n > destination.Length) n = destination.Length;
new ReadOnlySpan<byte>(GetBuffer(), _readPos, n).CopyTo(destination);
_readPos += n;
// We may have read less than the number of bytes the user asked
// for, but that is part of the Stream contract. Reading again for
// more data may cause us to block if we're using a device with
// no clear end of file, such as a serial port or pipe. If we
// blocked here & this code was used with redirected pipes for a
// process's standard output, this can lead to deadlocks involving
// two processes. But leave this here for files to avoid what would
// probably be a breaking change. --
// If we are reading from a device with no clear EOF like a
// serial port or a pipe, this will cause us to block incorrectly.
if (!_isPipe)
{
// If we hit the end of the buffer and didn't have enough bytes, we must
// read some more from the underlying stream. However, if we got
// fewer bytes from the underlying stream than we asked for (i.e. we're
// probably blocked), don't ask for more bytes.
if (n < destination.Length && !isBlocked)
{
Debug.Assert(_readPos == _readLength, "Read buffer should be empty!");
int moreBytesRead = ReadNative(destination.Slice(n));
n += moreBytesRead;
// We've just made our buffer inconsistent with our position
// pointer. We must throw away the read buffer.
_readPos = 0;
_readLength = 0;
}
}
return n;
}
[Conditional("DEBUG")]
private void AssertCanRead()
{
Debug.Assert(!_fileHandle.IsClosed, "!_fileHandle.IsClosed");
Debug.Assert(CanRead, "CanRead");
}
/// <summary>Reads from the file handle into the buffer, overwriting anything in it.</summary>
private int FillReadBufferForReadByte() =>
_useAsyncIO ?
ReadNativeAsync(new Memory<byte>(_buffer), 0, CancellationToken.None).GetAwaiter().GetResult() :
ReadNative(_buffer);
private unsafe int ReadNative(Span<byte> buffer)
{
Debug.Assert(!_useAsyncIO, $"{nameof(ReadNative)} doesn't work on asynchronous file streams.");
AssertCanRead();
// Make sure we are reading from the right spot
VerifyOSHandlePosition();
int r = ReadFileNative(_fileHandle, buffer, null, out int errorCode);
if (r == -1)
{
// For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe.
if (errorCode == ERROR_BROKEN_PIPE)
{
r = 0;
}
else
{
if (errorCode == ERROR_INVALID_PARAMETER)
throw new ArgumentException(SR.Arg_HandleNotSync, "_fileHandle");
throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path);
}
}
Debug.Assert(r >= 0, "FileStream's ReadNative is likely broken.");
_filePosition += r;
return r;
}
public override long Seek(long offset, SeekOrigin origin)
{
if (origin < SeekOrigin.Begin || origin > SeekOrigin.End)
throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin));
if (_fileHandle.IsClosed) throw Error.GetFileNotOpen();
if (!CanSeek) throw Error.GetSeekNotSupported();
Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both.");
// If we've got bytes in our buffer to write, write them out.
// If we've read in and consumed some bytes, we'll have to adjust
// our seek positions ONLY IF we're seeking relative to the current
// position in the stream. This simulates doing a seek to the new
// position, then a read for the number of bytes we have in our buffer.
if (_writePos > 0)
{
FlushWriteBuffer();
}
else if (origin == SeekOrigin.Current)
{
// Don't call FlushRead here, which would have caused an infinite
// loop. Simply adjust the seek origin. This isn't necessary
// if we're seeking relative to the beginning or end of the stream.
offset -= (_readLength - _readPos);
}
_readPos = _readLength = 0;
// Verify that internal position is in sync with the handle
VerifyOSHandlePosition();
long oldPos = _filePosition + (_readPos - _readLength);
long pos = SeekCore(_fileHandle, offset, origin);
// Prevent users from overwriting data in a file that was opened in
// append mode.
if (_appendStart != -1 && pos < _appendStart)
{
SeekCore(_fileHandle, oldPos, SeekOrigin.Begin);
throw new IOException(SR.IO_SeekAppendOverwrite);
}
// We now must update the read buffer. We can in some cases simply
// update _readPos within the buffer, copy around the buffer so our
// Position property is still correct, and avoid having to do more
// reads from the disk. Otherwise, discard the buffer's contents.
if (_readLength > 0)
{
// We can optimize the following condition:
// oldPos - _readPos <= pos < oldPos + _readLen - _readPos
if (oldPos == pos)
{
if (_readPos > 0)
{
Buffer.BlockCopy(GetBuffer(), _readPos, GetBuffer(), 0, _readLength - _readPos);
_readLength -= _readPos;
_readPos = 0;
}
// If we still have buffered data, we must update the stream's
// position so our Position property is correct.
if (_readLength > 0)
SeekCore(_fileHandle, _readLength, SeekOrigin.Current);
}
else if (oldPos - _readPos < pos && pos < oldPos + _readLength - _readPos)
{
int diff = (int)(pos - oldPos);
Buffer.BlockCopy(GetBuffer(), _readPos + diff, GetBuffer(), 0, _readLength - (_readPos + diff));
_readLength -= (_readPos + diff);
_readPos = 0;
if (_readLength > 0)
SeekCore(_fileHandle, _readLength, SeekOrigin.Current);
}
else
{
// Lose the read buffer.
_readPos = 0;
_readLength = 0;
}
Debug.Assert(_readLength >= 0 && _readPos <= _readLength, "_readLen should be nonnegative, and _readPos should be less than or equal _readLen");
Debug.Assert(pos == Position, "Seek optimization: pos != Position! Buffer math was mangled.");
}
return pos;
}
// This doesn't do argument checking. Necessary for SetLength, which must
// set the file pointer beyond the end of the file. This will update the
// internal position
private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false)
{
Debug.Assert(!fileHandle.IsClosed && _canSeek, "!fileHandle.IsClosed && _canSeek");
Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End, "origin >= SeekOrigin.Begin && origin <= SeekOrigin.End");
if (!Interop.Kernel32.SetFilePointerEx(fileHandle, offset, out long ret, (uint)origin))
{
if (closeInvalidHandle)
{
throw Win32Marshal.GetExceptionForWin32Error(GetLastWin32ErrorAndDisposeHandleIfInvalid(), _path);
}
else
{
throw Win32Marshal.GetExceptionForLastWin32Error(_path);
}
}
_filePosition = ret;
return ret;
}
partial void OnBufferAllocated()
{
Debug.Assert(_buffer != null);
Debug.Assert(_preallocatedOverlapped == null);
if (_useAsyncIO)
_preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, _buffer);
}
private void WriteSpan(ReadOnlySpan<byte> source)
{
Debug.Assert(!_useAsyncIO, "Must only be used when in synchronous mode");
if (_writePos == 0)
{
// Ensure we can write to the stream, and ready buffer for writing.
if (!CanWrite) throw Error.GetWriteNotSupported();
if (_readPos < _readLength) FlushReadBuffer();
_readPos = 0;
_readLength = 0;
}
// If our buffer has data in it, copy data from the user's array into
// the buffer, and if we can fit it all there, return. Otherwise, write
// the buffer to disk and copy any remaining data into our buffer.
// The assumption here is memcpy is cheaper than disk (or net) IO.
// (10 milliseconds to disk vs. ~20-30 microseconds for a 4K memcpy)
// So the extra copying will reduce the total number of writes, in
// non-pathological cases (i.e. write 1 byte, then write for the buffer
// size repeatedly)
if (_writePos > 0)
{
int numBytes = _bufferLength - _writePos; // space left in buffer
if (numBytes > 0)
{
if (numBytes >= source.Length)
{
source.CopyTo(GetBuffer().AsSpan(_writePos));
_writePos += source.Length;
return;
}
else
{
source.Slice(0, numBytes).CopyTo(GetBuffer().AsSpan(_writePos));
_writePos += numBytes;
source = source.Slice(numBytes);
}
}
// Reset our buffer. We essentially want to call FlushWrite
// without calling Flush on the underlying Stream.
WriteCore(new ReadOnlySpan<byte>(GetBuffer(), 0, _writePos));
_writePos = 0;
}
// If the buffer would slow writes down, avoid buffer completely.
if (source.Length >= _bufferLength)
{
Debug.Assert(_writePos == 0, "FileStream cannot have buffered data to write here! Your stream will be corrupted.");
WriteCore(source);
return;
}
else if (source.Length == 0)
{
return; // Don't allocate a buffer then call memcpy for 0 bytes.
}
// Copy remaining bytes into buffer, to write at a later date.
source.CopyTo(GetBuffer().AsSpan(_writePos));
_writePos = source.Length;
return;
}
private unsafe void WriteCore(ReadOnlySpan<byte> source)
{
Debug.Assert(!_useAsyncIO);
Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed");
Debug.Assert(CanWrite, "_parent.CanWrite");
Debug.Assert(_readPos == _readLength, "_readPos == _readLen");
// Make sure we are writing to the position that we think we are
VerifyOSHandlePosition();
int r = WriteFileNative(_fileHandle, source, null, out int errorCode);
if (r == -1)
{
// For pipes, ERROR_NO_DATA is not an error, but the pipe is closing.
if (errorCode == ERROR_NO_DATA)
{
r = 0;
}
else
{
// ERROR_INVALID_PARAMETER may be returned for writes
// where the position is too large or for synchronous writes
// to a handle opened asynchronously.
if (errorCode == ERROR_INVALID_PARAMETER)
throw new IOException(SR.IO_FileTooLongOrHandleNotSync);
throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path);
}
}
Debug.Assert(r >= 0, "FileStream's WriteCore is likely broken.");
_filePosition += r;
return;
}
private Task<int>? ReadAsyncInternal(Memory<byte> destination, CancellationToken cancellationToken, out int synchronousResult)
{
Debug.Assert(_useAsyncIO);
if (!CanRead) throw Error.GetReadNotSupported();
Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both.");
if (_isPipe)
{
// Pipes are tricky, at least when you have 2 different pipes
// that you want to use simultaneously. When redirecting stdout
// & stderr with the Process class, it's easy to deadlock your
// parent & child processes when doing writes 4K at a time. The
// OS appears to use a 4K buffer internally. If you write to a
// pipe that is full, you will block until someone read from
// that pipe. If you try reading from an empty pipe and
// Win32FileStream's ReadAsync blocks waiting for data to fill it's
// internal buffer, you will be blocked. In a case where a child
// process writes to stdout & stderr while a parent process tries
// reading from both, you can easily get into a deadlock here.
// To avoid this deadlock, don't buffer when doing async IO on
// pipes. But don't completely ignore buffered data either.
if (_readPos < _readLength)
{
int n = Math.Min(_readLength - _readPos, destination.Length);
new Span<byte>(GetBuffer(), _readPos, n).CopyTo(destination.Span);
_readPos += n;
synchronousResult = n;
return null;
}
else
{
Debug.Assert(_writePos == 0, "Win32FileStream must not have buffered write data here! Pipes should be unidirectional.");
synchronousResult = 0;
return ReadNativeAsync(destination, 0, cancellationToken);
}
}
Debug.Assert(!_isPipe, "Should not be a pipe.");
// Handle buffering.
if (_writePos > 0) FlushWriteBuffer();
if (_readPos == _readLength)
{
// I can't see how to handle buffering of async requests when
// filling the buffer asynchronously, without a lot of complexity.
// The problems I see are issuing an async read, we do an async
// read to fill the buffer, then someone issues another read
// (either synchronously or asynchronously) before the first one
// returns. This would involve some sort of complex buffer locking
// that we probably don't want to get into, at least not in V1.
// If we did a sync read to fill the buffer, we could avoid the
// problem, and any async read less than 64K gets turned into a
// synchronous read by NT anyways... --
if (destination.Length < _bufferLength)
{
Task<int> readTask = ReadNativeAsync(new Memory<byte>(GetBuffer()), 0, cancellationToken);
_readLength = readTask.GetAwaiter().GetResult();
int n = Math.Min(_readLength, destination.Length);
new Span<byte>(GetBuffer(), 0, n).CopyTo(destination.Span);
_readPos = n;
synchronousResult = n;
return null;
}
else
{
// Here we're making our position pointer inconsistent
// with our read buffer. Throw away the read buffer's contents.
_readPos = 0;
_readLength = 0;
synchronousResult = 0;
return ReadNativeAsync(destination, 0, cancellationToken);
}
}
else
{
int n = Math.Min(_readLength - _readPos, destination.Length);
new Span<byte>(GetBuffer(), _readPos, n).CopyTo(destination.Span);
_readPos += n;
if (n == destination.Length)
{
// Return a completed task
synchronousResult = n;
return null;
}
else
{
// For streams with no clear EOF like serial ports or pipes
// we cannot read more data without causing an app to block
// incorrectly. Pipes don't go down this path
// though. This code needs to be fixed.
// Throw away read buffer.
_readPos = 0;
_readLength = 0;
synchronousResult = 0;
return ReadNativeAsync(destination.Slice(n), n, cancellationToken);
}
}
}
private unsafe Task<int> ReadNativeAsync(Memory<byte> destination, int numBufferedBytesRead, CancellationToken cancellationToken)
{
AssertCanRead();
Debug.Assert(_useAsyncIO, "ReadNativeAsync doesn't work on synchronous file streams!");
// Create and store async stream class library specific data in the async result
FileStreamCompletionSource completionSource = FileStreamCompletionSource.Create(this, numBufferedBytesRead, destination);
NativeOverlapped* intOverlapped = completionSource.Overlapped;
// Calculate position in the file we should be at after the read is done
if (CanSeek)
{
long len = Length;
// Make sure we are reading from the position that we think we are
VerifyOSHandlePosition();
if (_filePosition + destination.Length > len)
{
if (_filePosition <= len)
{
destination = destination.Slice(0, (int)(len - _filePosition));
}
else
{
destination = default;
}
}
// Now set the position to read from in the NativeOverlapped struct
// For pipes, we should leave the offset fields set to 0.
intOverlapped->OffsetLow = unchecked((int)_filePosition);
intOverlapped->OffsetHigh = (int)(_filePosition >> 32);
// When using overlapped IO, the OS is not supposed to
// touch the file pointer location at all. We will adjust it
// ourselves. This isn't threadsafe.
// WriteFile should not update the file pointer when writing
// in overlapped mode, according to MSDN. But it does update
// the file pointer when writing to a UNC path!
// So changed the code below to seek to an absolute
// location, not a relative one. ReadFile seems consistent though.
SeekCore(_fileHandle, destination.Length, SeekOrigin.Current);
}
// queue an async ReadFile operation and pass in a packed overlapped
int r = ReadFileNative(_fileHandle, destination.Span, intOverlapped, out int errorCode);
// ReadFile, the OS version, will return 0 on failure. But
// my ReadFileNative wrapper returns -1. My wrapper will return
// the following:
// On error, r==-1.
// On async requests that are still pending, r==-1 w/ errorCode==ERROR_IO_PENDING
// on async requests that completed sequentially, r==0
// You will NEVER RELIABLY be able to get the number of bytes
// read back from this call when using overlapped structures! You must
// not pass in a non-null lpNumBytesRead to ReadFile when using
// overlapped structures! This is by design NT behavior.
if (r == -1)
{
// For pipes, when they hit EOF, they will come here.
if (errorCode == ERROR_BROKEN_PIPE)
{
// Not an error, but EOF. AsyncFSCallback will NOT be
// called. Call the user callback here.
// We clear the overlapped status bit for this special case.
// Failure to do so looks like we are freeing a pending overlapped later.
intOverlapped->InternalLow = IntPtr.Zero;
completionSource.SetCompletedSynchronously(0);
}
else if (errorCode != ERROR_IO_PENDING)
{
if (!_fileHandle.IsClosed && CanSeek) // Update Position - It could be anywhere.
{
SeekCore(_fileHandle, 0, SeekOrigin.Current);
}
completionSource.ReleaseNativeResource();
if (errorCode == ERROR_HANDLE_EOF)
{
throw Error.GetEndOfFile();
}
else
{
throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path);
}
}
else if (cancellationToken.CanBeCanceled) // ERROR_IO_PENDING
{
// Only once the IO is pending do we register for cancellation
completionSource.RegisterForCancellation(cancellationToken);
}
}
else
{
// Due to a workaround for a race condition in NT's ReadFile &
// WriteFile routines, we will always be returning 0 from ReadFileNative
// when we do async IO instead of the number of bytes read,
// irregardless of whether the operation completed
// synchronously or asynchronously. We absolutely must not
// set asyncResult._numBytes here, since will never have correct
// results.
}
return completionSource.Task;
}
private ValueTask WriteAsyncInternal(ReadOnlyMemory<byte> source, CancellationToken cancellationToken)
{
Debug.Assert(_useAsyncIO);
Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both.");
Debug.Assert(!_isPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional.");
if (!CanWrite) throw Error.GetWriteNotSupported();
bool writeDataStoredInBuffer = false;
if (!_isPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore)
{
// Ensure the buffer is clear for writing
if (_writePos == 0)
{
if (_readPos < _readLength)
{
FlushReadBuffer();
}
_readPos = 0;
_readLength = 0;
}
// Determine how much space remains in the buffer
int remainingBuffer = _bufferLength - _writePos;
Debug.Assert(remainingBuffer >= 0);
// Simple/common case:
// - The write is smaller than our buffer, such that it's worth considering buffering it.
// - There's no active flush operation, such that we don't have to worry about the existing buffer being in use.
// - And the data we're trying to write fits in the buffer, meaning it wasn't already filled by previous writes.
// In that case, just store it in the buffer.
if (source.Length < _bufferLength && !HasActiveBufferOperation && source.Length <= remainingBuffer)
{
source.Span.CopyTo(new Span<byte>(GetBuffer(), _writePos, source.Length));
_writePos += source.Length;
writeDataStoredInBuffer = true;
// There is one special-but-common case, common because devs often use
// byte[] sizes that are powers of 2 and thus fit nicely into our buffer, which is
// also a power of 2. If after our write the buffer still has remaining space,
// then we're done and can return a completed task now. But if we filled the buffer
// completely, we want to do the asynchronous flush/write as part of this operation
// rather than waiting until the next write that fills the buffer.
if (source.Length != remainingBuffer)
return default;
Debug.Assert(_writePos == _bufferLength);
}
}
// At this point, at least one of the following is true:
// 1. There was an active flush operation (it could have completed by now, though).
// 2. The data doesn't fit in the remaining buffer (or it's a pipe and we chose not to try).