/// <summary> /// Transfers up to <paramref name="length"/> data from the <paramref name="easy"/>'s /// request content (non-memory) stream to the buffer. /// </summary> /// <returns>The number of bytes transferred.</returns> private static ulong TransferDataFromRequestStream(IntPtr buffer, int length, EasyRequest easy) { MultiAgent multi = easy._associatedMultiAgent; // First check to see whether there's any data available from a previous asynchronous read request. // If there is, the transfer state's Task field will be non-null, with its Result representing // the number of bytes read. The Buffer will then contain all of that read data. If the Count // is 0, then this is the first time we're checking that Task, and so we populate the Count // from that read result. After that, we can transfer as much data remains between Offset and // Count. Multiple callbacks may pull from that one read. EasyRequest.SendTransferState sts = easy._sendTransferState; if (sts != null) { // Is there a previous read that may still have data to be consumed? if (sts._task != null) { if (!sts._task.IsCompleted) { // We have a previous read that's not yet completed. This should be quite rare, but it can // happen when we're unpaused prematurely, potentially due to the request still finishing // being sent as the server starts to send a response. Since we still have the outstanding // read, we simply re-pause. When the task completes (which could have happened immediately // after the check). the continuation we previously created will fire and queue an unpause. // Since all of this processing is single-threaded on the current thread, that unpause request // is guaranteed to happen after this re-pause. multi.VerboseTrace("Re-pausing reading after a spurious un-pause", easy: easy); return Interop.Http.CURL_READFUNC_PAUSE; } // Determine how many bytes were read on the last asynchronous read. // If nothing was read, then we're done and can simply return 0 to indicate // the end of the stream. int bytesRead = sts._task.GetAwaiter().GetResult(); // will throw if read failed Debug.Assert(bytesRead >= 0 && bytesRead <= sts._buffer.Length, "ReadAsync returned an invalid result length: " + bytesRead); if (bytesRead == 0) { multi.VerboseTrace("End of stream from stored task", easy: easy); sts.SetTaskOffsetCount(null, 0, 0); return 0; } // If Count is still 0, then this is the first time after the task completed // that we're examining the data: transfer the bytesRead to the Count. if (sts._count == 0) { multi.VerboseTrace("ReadAsync completed with bytes: " + bytesRead, easy: easy); sts._count = bytesRead; } // Now Offset and Count are both accurate. Determine how much data we can copy to libcurl... int availableData = sts._count - sts._offset; Debug.Assert(availableData > 0, "There must be some data still available."); // ... and copy as much of that as libcurl will allow. int bytesToCopy = Math.Min(availableData, length); Marshal.Copy(sts._buffer, sts._offset, buffer, bytesToCopy); multi.VerboseTrace("Copied " + bytesToCopy + " bytes from request stream", easy: easy); // Update the offset. If we've gone through all of the data, reset the state // so that the next time we're called back we'll do a new read. sts._offset += bytesToCopy; Debug.Assert(sts._offset <= sts._count, "Offset should never exceed count"); if (sts._offset == sts._count) { sts.SetTaskOffsetCount(null, 0, 0); } // Return the amount of data copied Debug.Assert(bytesToCopy > 0, "We should never return 0 bytes here."); return (ulong)bytesToCopy; } // sts was non-null but sts.Task was null, meaning there was no previous task/data // from which to satisfy any of this request. } else // sts == null { // Allocate a transfer state object to use for the remainder of this request. easy._sendTransferState = sts = new EasyRequest.SendTransferState(); } Debug.Assert(sts != null, "By this point we should have a transfer object"); Debug.Assert(sts._task == null, "There shouldn't be a task now."); Debug.Assert(sts._count == 0, "Count should be zero."); Debug.Assert(sts._offset == 0, "Offset should be zero."); // If we get here, there was no previously read data available to copy. // Initiate a new asynchronous read. Task<int> asyncRead = easy._requestContentStream.ReadAsyncInternal( sts._buffer, 0, Math.Min(sts._buffer.Length, length), easy._cancellationToken); Debug.Assert(asyncRead != null, "Badly implemented stream returned a null task from ReadAsync"); // Even though it's "Async", it's possible this read could complete synchronously or extremely quickly. // Check to see if it did, in which case we can also satisfy the libcurl request synchronously in this callback. if (asyncRead.IsCompleted) { multi.VerboseTrace("ReadAsync completed immediately", easy: easy); // Get the amount of data read. int bytesRead = asyncRead.GetAwaiter().GetResult(); // will throw if read failed if (bytesRead == 0) { multi.VerboseTrace("End of stream from quick returning ReadAsync", easy: easy); return 0; } // Copy as much as we can. int bytesToCopy = Math.Min(bytesRead, length); Debug.Assert(bytesToCopy > 0 && bytesToCopy <= sts._buffer.Length, "ReadAsync quickly returned an invalid result length: " + bytesToCopy); Marshal.Copy(sts._buffer, 0, buffer, bytesToCopy); multi.VerboseTrace("Copied " + bytesToCopy + " from quick returning ReadAsync", easy: easy); // If we read more than we were able to copy, stash it away for the next read. if (bytesToCopy < bytesRead) { multi.VerboseTrace("Stashing away " + (bytesRead - bytesToCopy) + " bytes for next read.", easy: easy); sts.SetTaskOffsetCount(asyncRead, bytesToCopy, bytesRead); } // Return the number of bytes read. return (ulong)bytesToCopy; } // Otherwise, the read completed asynchronously. Store the task, and hook up a continuation // such that the connection will be unpaused once the task completes. sts.SetTaskOffsetCount(asyncRead, 0, 0); asyncRead.ContinueWith((t, s) => { EasyRequest easyRef = (EasyRequest)s; easyRef._associatedMultiAgent.RequestUnpause(easyRef); }, easy, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); // Then pause the connection. multi.VerboseTrace("Pausing the connection", easy: easy); return Interop.Http.CURL_READFUNC_PAUSE; }