/// <inheritdoc /> protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { using (StreamOperationToken op = StartNewOperation(StreamOperation.Dispose)) { BackgroundOperationSlot slot = op.WaitForBackgroundOperationSlot(); if (slot.Usability != StreamUsability.Broken) { FlushOrDiscardBufferAsync(slot).GetAwaiter().GetResult(); } if (m_ownsFile) { m_file.Close(); } if (slot.Usability == StreamUsability.Broken) { throw slot.ThrowExceptionForBrokenStream(); } } } // TODO: Consider FailFast for the !disposing (finalizer) case. internalBuffer.Dispose(); }
/// <summary> /// Starts a read from the buffer. Returns a bool indicating if a fill started as a result. A fill is not started unless the stream is usable. /// </summary> private bool ReadBufferAndStartFillIfEmptiedButUsable(BackgroundOperationSlot backgroundOperationSlot, byte[] buffer, int offset, int count, out int bytesReadFromBuffer) { FileBuffer.BufferOperationStatus status = internalBuffer.Read(buffer, offset, count, out bytesReadFromBuffer); switch (status) { case FileBuffer.BufferOperationStatus.ReadExhausted: // We start a background fill if the stream is still usable, but we don't need to explicitly handle // EOF or broken-ness here: // - If the caller gets bytes from the buffer, let them get used (i.e., read buffer bytes before complaining about EOF). // - If the caller needs bytes right away but we don't have any, the caller should wait on the fill (if true is returned) // and check stream usability. // The key point is that a dedicated caller will eventually hit the second case. We defer EOF / failure until the caller // catches up to the stream position where it happens; if they do not ever read up to that point, pretend it never happened. if (backgroundOperationSlot.Usability == StreamUsability.Usable) { backgroundOperationSlot.StartBackgroundOperation(StreamBackgroundOperation.Fill); return(true); } else { return(false); } case FileBuffer.BufferOperationStatus.CapacityRemaining: Contract.Assume(bytesReadFromBuffer > 0); return(false); case FileBuffer.BufferOperationStatus.FlushRequired: Contract.Assume(false, "Buffer is used only for reads, and so never needs to be write-flushed."); throw new InvalidOperationException("Unreachable"); default: throw Contract.AssertFailure("Unhandled BufferOperationStatus"); } }
/// <inheritdoc /> public override async Task <int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { using (StreamOperationToken op = StartNewOperation(StreamOperation.Read)) { BackgroundOperationSlot backgroundOperationSlot = await op.WaitForBackgroundOperationSlotAsync(); Contract.Assume( internalBuffer.State != FileBuffer.BufferState.Locked, "Buffer should not be locked, since no background operation is running."); int bytesReadFromBuffer; bool fillStarted = ReadBufferAndStartFillIfEmptiedButUsable(backgroundOperationSlot, buffer, offset, count, out bytesReadFromBuffer); if (bytesReadFromBuffer == 0) { if (fillStarted) { backgroundOperationSlot = await op.WaitForBackgroundOperationSlotAsync(); } // We just ran a fill and waited for it (usability may be updated on its completion) or we were unable to start a fill. // In either case, we should now respond to usability. We don't have any bytes read, and so we are now in some sense at the position // where the unusability occurs, rather than behind it (e.g. we should quietly exhaust the buffer before complaining about EOF). switch (backgroundOperationSlot.Usability) { case StreamUsability.Usable: Contract.Assume( fillStarted, "ReadBufferAndStartFillIfEmptiedButUsable should have started a fill, since the stream is usable"); Analysis.IgnoreResult( ReadBufferAndStartFillIfEmptiedButUsable(backgroundOperationSlot, buffer, offset, count, out bytesReadFromBuffer)); Contract.Assume( bytesReadFromBuffer > 0, "Usable stream implies that the completed fill obtained bytes. Zero bytes returned from ReadFile implies failure."); break; case StreamUsability.EndOfFileReached: Contract.Assume( internalBuffer.State == FileBuffer.BufferState.Empty, "EndOfFileReached usability coincides with a totally-failed fill (nothing read)"); break; case StreamUsability.Broken: throw backgroundOperationSlot.ThrowExceptionForBrokenStream(); default: throw Contract.AssertFailure("Unhandled StreamUsability"); } } // We've satisfied a read request for 'bytesReadFromBuffer' bytes, which advances our virtual file position. op.AdvancePosition(bytesReadFromBuffer); return(bytesReadFromBuffer); } }
/// <summary> /// <see cref="Stream.Flush"/> /// </summary> public override void Flush() { using (StreamOperationToken op = StartNewOperation(StreamOperation.Flush)) { BackgroundOperationSlot slot = op.WaitForBackgroundOperationSlot(); if (slot.Usability == StreamUsability.Broken) { throw slot.ThrowExceptionForBrokenStream(); } // TODO: Shouldn't drop the read buffer unless seeking to a new position. FlushOrDiscardBufferAndResetPositionAsync(op, slot, m_position).GetAwaiter().GetResult(); } }
/// <summary> /// Starts a background operation. This causes the <see cref="internalBuffer"/> to fill or flush. /// It must be ensured that no other background operation will be in progress, and that the stream is still usable after it. /// - First start an operation with <see cref="StartNewOperation"/>; this reserves the right to start the next background operation. /// - Then, wait for the current background operation, if any. We've established another one will not follow it due to the prior point. /// - Then, respond to the current stream usability (EOF, broken, etc.) as updated by the prior operation. /// </summary> private unsafe void StartBackgroundOperation(BackgroundOperationSlot slot, StreamBackgroundOperation nextOperation) { Contract.Requires(nextOperation != StreamBackgroundOperation.None); Analysis.IgnoreArgument(slot); lock (m_backgroundOperationLock) { if (m_usability != StreamUsability.Usable) { Contract.Assume(false, "Attempting to start a background operation on an unusable stream: " + m_usability.ToString("G")); } Contract.Assume( m_currentBackgroundOperation == StreamBackgroundOperation.None, "Background operation already in progress; wait on it first?"); Contract.Assume(m_currentBackgroundOperationCompletionSource == null); m_currentBackgroundOperation = nextOperation; } // Now actually start the async operation. // Note that the callback to 'this' (IIOCompletionTarget) can happen on this same stack byte *pinnedBuffer; int operationLength; switch (nextOperation) { case StreamBackgroundOperation.Fill: internalBuffer.LockForFill(out pinnedBuffer, out operationLength); m_file.ReadOverlapped(this, pinnedBuffer, operationLength, m_bufferPosition); break; case StreamBackgroundOperation.Flush: internalBuffer.LockForFlush(out pinnedBuffer, out operationLength); m_file.WriteOverlapped(this, pinnedBuffer, operationLength, m_bufferPosition); break; default: throw Contract.AssertFailure("Unhandled StreamBackgroundOperation"); } }
/// <summary> /// <see cref="Stream.Seek(long, SeekOrigin)"/> /// </summary> public override long Seek(long offset, SeekOrigin origin) { using (StreamOperationToken token = StartNewOperation(StreamOperation.Seek)) { BackgroundOperationSlot slot = token.WaitForBackgroundOperationSlot(); if (slot.Usability == StreamUsability.Broken) { throw slot.ThrowExceptionForBrokenStream(); } long offsetFromStart; switch (origin) { case SeekOrigin.Begin: Contract.Assume(offset >= 0, "Attempted to seek to a negative offset"); offsetFromStart = offset; break; case SeekOrigin.Current: Contract.Assume(m_position >= offset, "Attempted to seek (relative to current) to a negative offset"); offsetFromStart = m_position + offset; break; case SeekOrigin.End: throw new NotSupportedException("Seeking relative to stream end is not supported"); default: throw Contract.AssertFailure("Unknwon SeekOrigin"); } if (m_position != offsetFromStart) { FlushOrDiscardBufferAndResetPositionAsync(token, slot, offsetFromStart).GetAwaiter().GetResult(); } return(offsetFromStart); } }
private async Task FlushOrDiscardBufferAndResetPositionAsync(StreamOperationToken token, BackgroundOperationSlot slot, long newPosition) { Contract.Requires(slot.Usability != StreamUsability.Broken); Analysis.IgnoreArgument(token); m_position = newPosition; await FlushOrDiscardBufferAsync(slot); lock (m_backgroundOperationLock) { // Buffer is now empty, and so its position should be in sync with the virtual position (recall how both begin at zero on stream open). m_bufferPosition = m_position; // Buffer position changed, so an end-of-file indication is no longer valid. if (m_usability == StreamUsability.EndOfFileReached) { m_usability = StreamUsability.Usable; } else { Contract.Assume(m_usability == StreamUsability.Usable, "m_usability == slot.Usability is not Broken"); } } }
/// <summary> /// Inheritor-provided implementation for a 'flush or discard' operation. /// A read-only stream discards while a write-only stream flushes. /// </summary> protected abstract Task FlushOrDiscardBufferAsync(BackgroundOperationSlot slot);
/// <inheritdoc /> protected override Task FlushOrDiscardBufferAsync(BackgroundOperationSlot slot) { Analysis.IgnoreArgument(slot); internalBuffer.Discard(); return(Unit.VoidTask); }