internal async Task InnerProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // Start other three unidirectional streams here.
            var controlTask = CreateControlStream(application);
            var encoderTask = CreateEncoderStream(application);
            var decoderTask = CreateDecoderStream(application);

            try
            {
                while (true)
                {
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    if (streamContext == null || _haveSentGoAway)
                    {
                        break;
                    }

                    var quicStreamFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                    var streamIdFeature   = streamContext.Features.Get <IStreamIdFeature>();

                    Debug.Assert(quicStreamFeature != null);

                    var httpConnectionContext = new Http3StreamContext(
                        streamContext.ConnectionId,
                        protocols: default,
Beispiel #2
0
        public static async Task <QuicStreamContext> CreateAndCompleteBidirectionalStreamGracefully(QuicConnection clientConnection, MultiplexedConnectionContext serverConnection)
        {
            var clientStream = clientConnection.OpenBidirectionalStream();
            await clientStream.WriteAsync(TestData, endStream : true).DefaultTimeout();

            var serverStream = await serverConnection.AcceptAsync().DefaultTimeout();

            var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();

            serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End);

            // Input should be completed.
            readResult = await serverStream.Transport.Input.ReadAsync();

            Assert.True(readResult.IsCompleted);

            // Complete reading and writing.
            await serverStream.Transport.Input.CompleteAsync();

            await serverStream.Transport.Output.CompleteAsync();

            var quicStreamContext = Assert.IsType <QuicStreamContext>(serverStream);

            // Both send and receive loops have exited.
            await quicStreamContext._processingTask.DefaultTimeout();

            Assert.True(quicStreamContext.CanWrite);
            Assert.True(quicStreamContext.CanRead);

            await quicStreamContext.DisposeAsync();

            quicStreamContext.Dispose();

            return(quicStreamContext);
        }
Beispiel #3
0
        public async Task GracefulServerShutdownSendsGoawayClosesConnection()
        {
            await InitializeConnectionAsync(_echoApplication);

            // Trigger server shutdown.
            MultiplexedConnectionContext.ConnectionClosingCts.Cancel();
            Assert.Null(await MultiplexedConnectionContext.AcceptAsync().DefaultTimeout());
        }
Beispiel #4
0
        public async Task GracefulServerShutdownClosesConnection()
        {
            await InitializeConnectionAsync(_echoApplication);

            var inboundControlStream = await GetInboundControlStream();

            await inboundControlStream.ExpectSettingsAsync();

            // Trigger server shutdown.
            CloseConnectionGracefully();

            Assert.Null(await MultiplexedConnectionContext.AcceptAsync().DefaultTimeout());

            await WaitForConnectionStopAsync(0, false, expectedErrorCode : Http3ErrorCode.NoError);
        }
Beispiel #5
0
    public static async Task <QuicStreamContext> CreateAndCompleteBidirectionalStreamGracefully(QuicConnection clientConnection, MultiplexedConnectionContext serverConnection, ILogger logger)
    {
        logger.LogInformation("Client starting stream.");
        var clientStream = await clientConnection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional);

        logger.LogInformation("Client sending data.");
        await clientStream.WriteAsync(TestData, completeWrites : true).DefaultTimeout();

        logger.LogInformation("Server accepting stream.");
        var serverStream = await serverConnection.AcceptAsync().DefaultTimeout();

        logger.LogInformation("Server reading data.");
        var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();

        serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End);

        // Input should be completed.
        readResult = await serverStream.Transport.Input.ReadAsync();

        Assert.True(readResult.IsCompleted);

        // Complete reading and writing.
        logger.LogInformation("Server completing input and output.");
        await serverStream.Transport.Input.CompleteAsync();

        await serverStream.Transport.Output.CompleteAsync();

        var quicStreamContext = Assert.IsType <QuicStreamContext>(serverStream);

        // Both send and receive loops have exited.
        logger.LogInformation("Server verifying stream is finished.");
        await quicStreamContext._processingTask.DefaultTimeout();

        Assert.True(quicStreamContext.CanWrite);
        Assert.True(quicStreamContext.CanRead);

        logger.LogInformation("Server disposing stream.");
        await quicStreamContext.DisposeAsync();

        quicStreamContext.Dispose();

        return(quicStreamContext);
    }
Beispiel #6
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.

            Exception?         error = null;
            Http3ControlStream?outboundControlStream     = null;
            ValueTask          outboundControlStreamTask = default;
            bool clientAbort = false;

            try
            {
                outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);

                lock (_sync)
                {
                    OutboundControlStream = outboundControlStream;
                }

                // Don't delay on waiting to send outbound control stream settings.
                outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

                while (_stoppedAcceptingStreams == 0)
                {
                    var streamContext = await _multiplexedContext.AcceptAsync(_acceptStreamsCts.Token);

                    try
                    {
                        // Return null on server close or cancellation.
                        if (streamContext == null)
                        {
                            if (_acceptStreamsCts.Token.IsCancellationRequested)
                            {
                                _acceptStreamsCts = new CancellationTokenSource();
                            }

                            // There is no stream so continue to skip to UpdateConnectionState in finally.
                            // UpdateConnectionState is responsible for updating connection to
                            // stop accepting streams and break out of accept loop.
                            continue;
                        }

                        var streamDirectionFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature        = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(streamDirectionFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        if (!streamDirectionFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, CreateHttpStreamContext(streamContext));
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            // Request stream

                            // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-5.2-2
                            if (_gracefulCloseStarted)
                            {
                                // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.2-3
                                streamContext.Features.Get <IProtocolErrorCodeFeature>() !.Error = (long)Http3ErrorCode.RequestRejected;
                                streamContext.Abort(new ConnectionAbortedException("HTTP/3 connection is closing and no longer accepts new requests."));
                                await streamContext.DisposeAsync();

                                continue;
                            }

                            // Request stream IDs are tracked.
                            UpdateHighestOpenedRequestStreamId(streamIdFeature.StreamId);

                            var persistentStateFeature = streamContext.Features.Get <IPersistentStateFeature>();
                            Debug.Assert(persistentStateFeature != null, $"Required {nameof(IPersistentStateFeature)} not on stream context.");

                            Http3Stream <TContext> stream;

                            // Check whether there is an existing HTTP/3 stream on the transport stream.
                            // A stream will only be cached if the transport stream itself is reused.
                            if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s))
                            {
                                stream = new Http3Stream <TContext>(application, CreateHttpStreamContext(streamContext));
                                persistentStateFeature.State.Add(StreamPersistentStateKey, stream);
                            }
                            else
                            {
                                stream = (Http3Stream <TContext>)s !;
                                stream.InitializeWithExistingContext(streamContext.Transport);
                            }

                            _streamLifetimeHandler.OnStreamCreated(stream);

                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
                    finally
                    {
                        UpdateConnectionState();
                    }
                }
            }
            catch (ConnectionResetException ex)
            {
                lock (_streams)
                {
                    if (_activeRequestCount > 0)
                    {
                        Log.RequestProcessingError(_context.ConnectionId, ex);
                    }
                }
                error       = ex;
                clientAbort = true;
            }
            catch (IOException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (ConnectionAbortedException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Http3ConnectionErrorException ex)
            {
                Log.Http3ConnectionError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                try
                {
                    // Don't try to send GOAWAY if the client has already closed the connection.
                    if (!clientAbort)
                    {
                        if (TryStopAcceptingStreams() || _gracefulCloseStarted)
                        {
                            await SendGoAwayAsync(GetCurrentGoAwayStreamId());
                        }
                    }

                    // Abort active request streams.
                    lock (_streams)
                    {
                        foreach (var stream in _streams.Values)
                        {
                            stream.Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);
                        }
                    }

                    if (outboundControlStream != null)
                    {
                        // Don't gracefully close the outbound control stream. If the peer detects
                        // the control stream closes it will close with a procotol error.
                        // Instead, allow control stream to be automatically aborted when the
                        // connection is aborted.
                        await outboundControlStreamTask;
                    }

                    // Complete
                    Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);

                    // Wait for active requests to complete.
                    while (_activeRequestCount > 0)
                    {
                        await _streamCompletionAwaitable;
                    }

                    _context.TimeoutControl.CancelTimeout();
                }
                catch
                {
                    Abort(CreateConnectionAbortError(error, clientAbort), Http3ErrorCode.InternalError);
                    throw;
                }
                finally
                {
                    // Connection can close without processing any request streams.
                    var streamId = _highestOpenedRequestStreamId != DefaultHighestOpenedRequestStreamId
                        ? _highestOpenedRequestStreamId
                        : (long?)null;

                    Log.Http3ConnectionClosed(_context.ConnectionId, streamId);
                }
            }
        }
Beispiel #7
0
        internal async Task InnerProcessRequestsAsync <TContext>(IHttpApplication <TContext> application)
        {
            // Start other three unidirectional streams here.
            var controlTask = CreateControlStream(application);
            var encoderTask = CreateEncoderStream(application);
            var decoderTask = CreateDecoderStream(application);

            try
            {
                while (true)
                {
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    if (streamContext == null || _haveSentGoAway)
                    {
                        break;
                    }

                    var quicStreamFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                    var streamIdFeature   = streamContext.Features.Get <IStreamIdFeature>();

                    Debug.Assert(quicStreamFeature != null);

                    var httpConnectionContext = new Http3StreamContext
                    {
                        ConnectionId  = streamContext.ConnectionId,
                        StreamContext = streamContext,
                        // TODO connection context is null here. Should we set it to anything?
                        ServiceContext     = _context.ServiceContext,
                        ConnectionFeatures = streamContext.Features,
                        MemoryPool         = _context.MemoryPool,
                        Transport          = streamContext.Transport,
                        TimeoutControl     = _context.TimeoutControl,
                        LocalEndPoint      = streamContext.LocalEndPoint as IPEndPoint,
                        RemoteEndPoint     = streamContext.RemoteEndPoint as IPEndPoint
                    };

                    if (!quicStreamFeature.CanWrite)
                    {
                        // Unidirectional stream
                        var stream = new Http3ControlStream <TContext>(application, this, httpConnectionContext);
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                    else
                    {
                        // Keep track of highest stream id seen for GOAWAY
                        var streamId = streamIdFeature.StreamId;
                        HighestStreamId = streamId;

                        var http3Stream = new Http3Stream <TContext>(application, this, httpConnectionContext);
                        var stream      = http3Stream;
                        lock (_streams)
                        {
                            _streams[streamId] = http3Stream;
                        }
                        KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                        ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                    }
                }
            }
            finally
            {
                // Abort all streams as connection has shutdown.
                lock (_streams)
                {
                    foreach (var stream in _streams.Values)
                    {
                        stream.Abort(new ConnectionAbortedException("Connection is shutting down."));
                    }
                }

                ControlStream?.Abort(new ConnectionAbortedException("Connection is shutting down."));
                EncoderStream?.Abort(new ConnectionAbortedException("Connection is shutting down."));
                DecoderStream?.Abort(new ConnectionAbortedException("Connection is shutting down."));

                await controlTask;
                await encoderTask;
                await decoderTask;
            }
        }
        internal async Task InnerProcessStreamsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.
            Exception?error = null;

            // TODO should we await the control stream task?
            var controlTask = CreateControlStream(application);

            _timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);

            try
            {
                while (_isClosed == 0)
                {
                    // TODO implement way to unblock loop for one call to accept async to update state.
                    // Use cts for now, update to custom awaitable or different solution in the future.
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    try
                    {
                        if (streamContext == null)
                        {
                            break;
                        }

                        var quicStreamFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature   = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(quicStreamFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        var httpConnectionContext = new Http3StreamContext(
                            streamContext.ConnectionId,
                            protocols: default,
                            connectionContext: null !, // TODO connection context is null here. Should we set it to anything?
                            _context.ServiceContext,
                            streamContext.Features,
                            _context.MemoryPool,
                            streamContext.LocalEndPoint as IPEndPoint,
                            streamContext.RemoteEndPoint as IPEndPoint,
                            streamContext.Transport,
                            streamContext,
                            _serverSettings);
                        httpConnectionContext.TimeoutControl = _context.TimeoutControl;

                        if (!quicStreamFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, this, httpConnectionContext);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            var streamId = streamIdFeature.StreamId;

                            HighestStreamId = streamId;

                            var http3Stream = new Http3Stream <TContext>(application, this, httpConnectionContext);
                            var stream      = http3Stream;
                            lock (_streams)
                            {
                                _activeRequestCount++;
                                _streams[streamId] = http3Stream;
                            }
                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
Beispiel #9
0
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.

            Exception?error = null;
            ValueTask outboundControlStreamTask = default;

            try
            {
                var outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);

                lock (_sync)
                {
                    OutboundControlStream = outboundControlStream;
                }

                // Don't delay on waiting to send outbound control stream settings.
                outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

                while (_isClosed == 0)
                {
                    // Don't pass a cancellation token to AcceptAsync.
                    // AcceptAsync will return null if the connection is gracefully shutting down or aborted.
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    try
                    {
                        if (streamContext == null)
                        {
                            break;
                        }

                        var streamDirectionFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature        = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(streamDirectionFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        if (!streamDirectionFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, CreateHttpStreamContext(streamContext));
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            var persistentStateFeature = streamContext.Features.Get <IPersistentStateFeature>();
                            Debug.Assert(persistentStateFeature != null, $"Required {nameof(IPersistentStateFeature)} not on stream context.");

                            // Request stream
                            UpdateHighestStreamId(streamIdFeature.StreamId);

                            Http3Stream <TContext> stream;

                            // Check whether there is an existing HTTP/3 stream on the transport stream.
                            // A stream will only be cached if the transport stream itself is reused.
                            if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s))
                            {
                                stream = new Http3Stream <TContext>(application, CreateHttpStreamContext(streamContext));
                                persistentStateFeature.State.Add(StreamPersistentStateKey, stream);
                            }
                            else
                            {
                                stream = (Http3Stream <TContext>)s !;
                                stream.InitializeWithExistingContext(streamContext.Transport);
                            }

                            _streamLifetimeHandler.OnStreamCreated(stream);

                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }
                    finally
                    {
                        UpdateConnectionState();
                    }
                }
            }
            catch (ConnectionResetException ex)
            {
                lock (_streams)
                {
                    if (_activeRequestCount > 0)
                    {
                        Log.RequestProcessingError(_context.ConnectionId, ex);
                    }
                }
                error = ex;
            }
            catch (IOException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (ConnectionAbortedException ex)
            {
                Log.RequestProcessingError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Http3ConnectionErrorException ex)
            {
                Log.Http3ConnectionError(_context.ConnectionId, ex);
                error = ex;
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                var connectionError = error as ConnectionAbortedException
                                      ?? new ConnectionAbortedException(CoreStrings.Http3ConnectionFaulted, error !);

                try
                {
                    if (TryClose())
                    {
                        // This throws when connection is shut down.
                        // TODO how to make it so we can distinguish between Abort from server vs client?
                        await SendGoAway(GetHighestStreamId());
                    }

                    foreach (var stream in _streams.Values)
                    {
                        stream.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
                    }

                    lock (_sync)
                    {
                        OutboundControlStream?.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
                    }

                    while (_activeRequestCount > 0)
                    {
                        await _streamCompletionAwaitable;
                    }

                    await outboundControlStreamTask;

                    _context.TimeoutControl.CancelTimeout();
                    _context.TimeoutControl.StartDrainTimeout(Limits.MinResponseDataRate, Limits.MaxResponseBufferSize);
                }
                catch
                {
                    Abort(connectionError, Http3ErrorCode.InternalError);
                    throw;
                }
            }
        }
        public async Task ProcessRequestsAsync <TContext>(IHttpApplication <TContext> application) where TContext : notnull
        {
            // An endpoint MAY avoid creating an encoder stream if it's not going to
            // be used(for example if its encoder doesn't wish to use the dynamic
            // table, or if the maximum size of the dynamic table permitted by the
            // peer is zero).

            // An endpoint MAY avoid creating a decoder stream if its decoder sets
            // the maximum capacity of the dynamic table to zero.

            // Don't create Encoder and Decoder as they aren't used now.

            Exception?error = null;
            ValueTask outboundControlStreamTask = default;

            try
            {
                var outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);

                lock (_sync)
                {
                    OutboundControlStream = outboundControlStream;
                }

                // Don't delay on waiting to send outbound control stream settings.
                outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);

                while (_isClosed == 0)
                {
                    // Don't pass a cancellation token to AcceptAsync.
                    // AcceptAsync will return null if the connection is gracefully shutting down or aborted.
                    var streamContext = await _multiplexedContext.AcceptAsync();

                    try
                    {
                        if (streamContext == null)
                        {
                            break;
                        }

                        var streamDirectionFeature = streamContext.Features.Get <IStreamDirectionFeature>();
                        var streamIdFeature        = streamContext.Features.Get <IStreamIdFeature>();

                        Debug.Assert(streamDirectionFeature != null);
                        Debug.Assert(streamIdFeature != null);

                        var httpConnectionContext = new Http3StreamContext(
                            streamContext.ConnectionId,
                            protocols: default,
                            connectionContext: null !, // TODO connection context is null here. Should we set it to anything?
                            _context.ServiceContext,
                            streamContext.Features,
                            _context.MemoryPool,
                            streamContext.LocalEndPoint as IPEndPoint,
                            streamContext.RemoteEndPoint as IPEndPoint,
                            streamContext.Transport,
                            _streamLifetimeHandler,
                            streamContext,
                            _clientSettings,
                            _serverSettings);
                        httpConnectionContext.TimeoutControl = _context.TimeoutControl;

                        if (!streamDirectionFeature.CanWrite)
                        {
                            // Unidirectional stream
                            var stream = new Http3ControlStream <TContext>(application, httpConnectionContext);
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                        else
                        {
                            // Request stream
                            UpdateHighestStreamId(streamIdFeature.StreamId);

                            var stream = new Http3Stream <TContext>(application, httpConnectionContext);
                            _streamLifetimeHandler.OnStreamCreated(stream);

                            KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
                            ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
                        }
                    }