/// <summary> /// Get an <see cref="IObservable{T}"/> for lines streamed from an HTTP GET request. /// </summary> /// <param name="request"> /// The <see cref="HttpRequest"/> to execute. /// </param> /// <param name="operationDescription"> /// A short description of the operation (used in error messages if the request fails). /// </param> /// <param name="bufferSize"> /// The buffer size to use when streaming data. /// /// Default is 2048 bytes. /// </param> /// <returns> /// The <see cref="IObservable{T}"/>. /// </returns> protected IObservable <string> ObserveLines(HttpRequest request, string operationDescription, int bufferSize = DefaultStreamingBufferSize) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (String.IsNullOrWhiteSpace(operationDescription)) { throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'operationDescription'.", nameof(operationDescription)); } return(Observable.Create <string>(async(subscriber, cancellationToken) => { try { using (HttpResponseMessage responseMessage = await Http.GetStreamedAsync(request, cancellationToken).ConfigureAwait(false)) { if (!responseMessage.IsSuccessStatusCode) { throw HttpRequestException <StatusV1> .Create(responseMessage.StatusCode, await responseMessage.ReadContentAsStatusV1Async().ConfigureAwait(false) ); } MediaTypeHeaderValue contentTypeHeader = responseMessage.Content.Headers.ContentType; if (contentTypeHeader == null) { throw new KubeClientException($"Unable to {operationDescription} (response is missing 'Content-Type' header)."); } Encoding encoding = !String.IsNullOrWhiteSpace(contentTypeHeader.CharSet) ? Encoding.GetEncoding(contentTypeHeader.CharSet) : Encoding.UTF8; Decoder decoder = encoding.GetDecoder(); using (Stream responseStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false)) { StringBuilder lineBuilder = new StringBuilder(); byte[] buffer = new byte[bufferSize]; int bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); while (bytesRead > 0) { // AF: Slightly inefficient because we wind up scanning the buffer twice. char[] decodedCharacters = new char[decoder.GetCharCount(buffer, 0, bytesRead)]; int charactersDecoded = decoder.GetChars(buffer, 0, bytesRead, decodedCharacters, 0); for (int charIndex = 0; charIndex < charactersDecoded; charIndex++) { const char CR = '\r'; const char LF = '\n'; char decodedCharacter = decodedCharacters[charIndex]; switch (decodedCharacter) { case CR: { if (charIndex < charactersDecoded - 1 && decodedCharacters[charIndex + 1] == LF) { charIndex++; goto case LF; } break; } case LF: { string line = lineBuilder.ToString(); lineBuilder.Clear(); subscriber.OnNext(line); break; } default: { lineBuilder.Append(decodedCharacter); break; } } } bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); } // If stream doesn't end with a line-terminator sequence, publish trailing characters as the last line. if (lineBuilder.Length > 0) { subscriber.OnNext( lineBuilder.ToString() ); } } } } catch (OperationCanceledException operationCanceled) when(operationCanceled.CancellationToken != cancellationToken) { if (!cancellationToken.IsCancellationRequested) // Don't bother publishing if subscriber has already disconnected. { subscriber.OnError(operationCanceled); } } catch (HttpRequestException <StatusV1> requestError) { if (!cancellationToken.IsCancellationRequested) { subscriber.OnError( new KubeClientException($"Unable to {operationDescription} (unexpected error while streaming from the Kubernetes API).", requestError) ); } } catch (Exception exception) { if (!cancellationToken.IsCancellationRequested) // Don't bother publishing if subscriber has already disconnected. { subscriber.OnError(exception); } } finally { if (!cancellationToken.IsCancellationRequested) // Don't bother publishing if subscriber has already disconnected. { subscriber.OnCompleted(); } } })); }