Example #1
0
        /// <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();
                    }
                }
            }));
        }