diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/corelib/Qt6AndroidGradleHelpers.cmake | 4 | ||||
| -rw-r--r-- | src/corelib/Qt6AndroidMacros.cmake | 3 | ||||
| -rw-r--r-- | src/corelib/kernel/qcoreapplication.cpp | 16 | ||||
| -rw-r--r-- | src/corelib/thread/qthread_p.h | 10 | ||||
| -rw-r--r-- | src/corelib/thread/qthread_unix.cpp | 219 | ||||
| -rw-r--r-- | src/corelib/thread/qthread_win.cpp | 5 | ||||
| -rw-r--r-- | src/network/access/qhttp2connection.cpp | 222 | ||||
| -rw-r--r-- | src/network/access/qhttp2connection_p.h | 22 | ||||
| -rw-r--r-- | src/tools/syncqt/main.cpp | 2 | ||||
| -rw-r--r-- | src/widgets/widgets/qtabbar.cpp | 6 |
10 files changed, 338 insertions, 171 deletions
diff --git a/src/corelib/Qt6AndroidGradleHelpers.cmake b/src/corelib/Qt6AndroidGradleHelpers.cmake index fc8e009b9da..136a61abbcd 100644 --- a/src/corelib/Qt6AndroidGradleHelpers.cmake +++ b/src/corelib/Qt6AndroidGradleHelpers.cmake @@ -6,7 +6,7 @@ function(_qt_internal_android_get_template_path out_var target template_name) if(template_name STREQUAL "") message(FATAL_ERROR "Template name is empty." - " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + " This is a Qt issue, please report a bug at https://bugreports.qt.io.") endif() _qt_internal_android_template_dir(template_directory) @@ -36,7 +36,7 @@ function(_qt_internal_android_get_template_path out_var target template_name) if(template_path STREQUAL "") message(FATAL_ERROR "'${template_name}' is not found." - " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + " This is a Qt issue, please report a bug at https://bugreports.qt.io.") endif() set(${out_var} "${template_path}" PARENT_SCOPE) diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index be362ba1925..ddc531a89f4 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -1790,10 +1790,11 @@ function(_qt_internal_android_app_runner_arguments target out_runner_path out_ar set(${out_runner_path} "${runner_dir}/qt-android-runner.py" PARENT_SCOPE) _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + _qt_internal_android_get_target_deployment_dir(android_deployment_dir ${target}) _qt_internal_android_get_platform_tools_path(platform_tools) set(${out_arguments} "--adb" "${platform_tools}/adb" - "--build-path" "${android_build_dir}" + "--build-path" "${android_deployment_dir}" "--apk" "${android_build_dir}/${target}.apk" PARENT_SCOPE ) diff --git a/src/corelib/kernel/qcoreapplication.cpp b/src/corelib/kernel/qcoreapplication.cpp index afc85fe36fb..19f5596880a 100644 --- a/src/corelib/kernel/qcoreapplication.cpp +++ b/src/corelib/kernel/qcoreapplication.cpp @@ -2913,7 +2913,6 @@ void QCoreApplication::requestPermissionImpl(const QPermission &requestedPermiss void *args[] = { nullptr, const_cast<QPermission *>(&permission) }; slotObject->call(const_cast<QObject *>(context.data()), args); } - deleteLater(); } private: @@ -2921,9 +2920,11 @@ void QCoreApplication::requestPermissionImpl(const QPermission &requestedPermiss QPointer<const QObject> context; }; - PermissionReceiver *receiver = new PermissionReceiver(std::move(slotObj), context); + // ### use unique_ptr once PermissionCallback is a move_only function + auto receiver = std::make_shared<PermissionReceiver>(std::move(slotObj), context); - QPermissions::Private::requestPermission(requestedPermission, [=](Qt::PermissionStatus status) { + QPermissions::Private::requestPermission(requestedPermission, + [=, receiver = std::move(receiver)](Qt::PermissionStatus status) mutable { if (status == Qt::PermissionStatus::Undetermined) { Q_ASSERT_X(false, "QPermission", "Internal error: requestPermission() should never return Undetermined"); @@ -2933,10 +2934,11 @@ void QCoreApplication::requestPermissionImpl(const QPermission &requestedPermiss if (QCoreApplication::self) { QPermission permission = requestedPermission; permission.m_status = status; - QMetaObject::invokeMethod(receiver, - &PermissionReceiver::finalizePermissionRequest, - Qt::QueuedConnection, - permission); + auto receiverObject = receiver.get(); + QMetaObject::invokeMethod(receiverObject, + [receiver = std::move(receiver), permission] { + receiver->finalizePermissionRequest(permission); + }, Qt::QueuedConnection); } }); } diff --git a/src/corelib/thread/qthread_p.h b/src/corelib/thread/qthread_p.h index 51d893a051d..41c36e99d3b 100644 --- a/src/corelib/thread/qthread_p.h +++ b/src/corelib/thread/qthread_p.h @@ -265,10 +265,16 @@ public: QBindingStatus *addObjectWithPendingBindingStatusChange(QObject *obj); void removeObjectWithPendingBindingStatusChange(QObject *obj); -#ifndef Q_OS_INTEGRITY private: +#ifdef Q_OS_INTEGRITY + // On INTEGRITY we set the thread name before starting it, so just fake a string + struct FakeString { + bool isEmpty() const { return true; } + const char *toLocal8Bit() const { return nullptr; } + } objectName; +#else // Used in QThread(Private)::start to avoid racy access to QObject::objectName, - // unset afterwards. On INTEGRITY we set the thread name before starting it. + // unset afterwards. QString objectName; #endif }; diff --git a/src/corelib/thread/qthread_unix.cpp b/src/corelib/thread/qthread_unix.cpp index bdab8dfb028..f53605367d7 100644 --- a/src/corelib/thread/qthread_unix.cpp +++ b/src/corelib/thread/qthread_unix.cpp @@ -24,10 +24,6 @@ # endif #endif -#ifdef __GLIBCXX__ -#include <cxxabi.h> -#endif - #include <sched.h> #include <errno.h> #if __has_include(<pthread_np.h>) @@ -344,59 +340,95 @@ QAbstractEventDispatcher *QThreadPrivate::createEventDispatcher(QThreadData *dat #if QT_CONFIG(thread) -#if (defined(Q_OS_LINUX) || defined(Q_OS_DARWIN) || defined(Q_OS_QNX)) -static void setCurrentThreadName(const char *name) +template <typename String> +static void setCurrentThreadName(QThread *thr, String &objectName) { -# if defined(Q_OS_LINUX) && !defined(QT_LINUXBASE) - prctl(PR_SET_NAME, (unsigned long)name, 0, 0, 0); + auto setit = [](const char *name) { +# if defined(Q_OS_LINUX) + prctl(PR_SET_NAME, name); # elif defined(Q_OS_DARWIN) - pthread_setname_np(name); + pthread_setname_np(name); # elif defined(Q_OS_QNX) - pthread_setname_np(pthread_self(), name); + pthread_setname_np(pthread_self(), name); +# else + Q_UNUSED(name) # endif + }; + if (Q_LIKELY(objectName.isEmpty())) + setit(thr->metaObject()->className()); + else + setit(std::exchange(objectName, {}).toLocal8Bit()); } -#endif -namespace { -#if defined(__GLIBCXX__) && !defined(QT_NO_EXCEPTIONS) -template <typename T> -void terminate_on_exception(T &&t) +// Handling of exceptions and cancellations for start(), finish() and cleanup() +// +// These routines expect that the user code throw no exceptions. Exiting +// start() with an exception should cause std::terminate to be called. Thread +// cancellations are allowed: if one is detected, the implementation is +// expected to cleanly call QThreadPrivate::finish(), emit the necessary +// signals and notifications, and clean up after itself. [Note there's a small +// race between QThread::start() returning and QThreadPrivate::start() turning +// cancellations off, during which time no finish() is called.] +// +// These routines implement application termination by unexpected exceptions by +// simply not having any try/catch block at all. As start() is called directly +// from the C library's PThread runtime, there should be no active C++ +// try/catch block (### if we ever change this to std::thread, the assumption +// needs to be rechecked, though both libc++ and libstdc++ at the time of +// writing are try/catch-free). [except.handle]/8 says: +// +// > If no matching handler is found, the function std::terminate is invoked; +// > whether or not the stack is unwound before this invocation of std::terminate +// > is implementation-defined. +// +// Both major implementations of Unix C++ Standard Libraries terminate without +// unwinding, which is useful to detect the unhandled exception in post-mortem +// debugging. This code adds no try/catch to retain that ability. +// +// Because of that, we could have marked these functions noexcept and ignored +// exception safety. We don't because of PThread cancellations. The GNU libc +// implements PThread cancellations using stack unwinding, so a cancellation +// *will* unwind the stack and *will* execute our C++ destructors, unlike +// exceptions. Therefore, our code in start() must be exception-safe after we +// turn cancellations back on, and until we turn them off again in finish(). +// +// Everywhere else, PThread cancellations are handled without unwinding the +// stack. + +static void setCancellationEnabled(bool enable) { - try { - std::forward<T>(t)(); - } catch (abi::__forced_unwind &) { - // POSIX thread cancellation under glibc is implemented by throwing an exception - // of this type. Do what libstdc++ is doing and handle it specially in order not to - // abort the application if user's code calls a cancellation function. - throw; - } catch (...) { - std::terminate(); +#ifdef PTHREAD_CANCEL_DISABLE + if (enable) { + // may unwind the stack, see above + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); + pthread_testcancel(); + } else { + // this doesn't unwind the stack + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr); } -} #else -template <typename T> -void terminate_on_exception(T &&t) noexcept -{ - std::forward<T>(t)(); + Q_UNUSED(enable) +#endif } -#endif // defined(__GLIBCXX__) && !defined(QT_NO_EXCEPTIONS) -} // unnamed namespace void *QThreadPrivate::start(void *arg) { -#ifdef PTHREAD_CANCEL_DISABLE - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr); -#endif + setCancellationEnabled(false); + QThread *thr = reinterpret_cast<QThread *>(arg); QThreadData *data = QThreadData::get2(thr); - // If a QThread is restarted, reuse the QBindingStatus, too - data->reuseBindingStatusForNewNativeThread(); // this ensures the thread-local is created as early as possible set_thread_data(data); + // If a QThread is restarted, reuse the QBindingStatus, too + data->reuseBindingStatusForNewNativeThread(); + pthread_cleanup_push([](void *arg) { static_cast<QThread *>(arg)->d_func()->finish(); }, arg); - terminate_on_exception([&] { + { // pthread cancellation protection + + // The functions called in this block do not usually throw (but have + // qWarning/qCDebug, which may throw std::bad_alloc). { QMutexLocker locker(&thr->d_func()->mutex); @@ -404,10 +436,6 @@ void *QThreadPrivate::start(void *arg) if (thr->d_func()->priority & ThreadPriorityResetFlag) { thr->d_func()->setPriority(QThread::Priority(thr->d_func()->priority & ~ThreadPriorityResetFlag)); } -#ifndef Q_OS_DARWIN // For Darwin we set it as an attribute when starting the thread - if (thr->d_func()->serviceLevel != QThread::QualityOfService::Auto) - thr->d_func()->setQualityOfServiceLevel(thr->d_func()->serviceLevel); -#endif // threadId is set in QThread::start() Q_ASSERT(data->threadId.loadRelaxed() == QThread::currentThreadId()); @@ -416,28 +444,25 @@ void *QThreadPrivate::start(void *arg) data->quitNow = thr->d_func()->exited; } + // Sets the name of the current thread. We can only do this + // when the thread is starting, as we don't have a cross + // platform way of setting the name of an arbitrary thread. + setCurrentThreadName(thr, thr->d_func()->objectName); + + // Re-enable cancellations before calling out to user code in run(), + // allowing the event dispatcher to abort this thread starting (exceptions + // aren't allowed to do that). This will also deliver a pending + // cancellation queued either by a slot connected to started() or by + // another thread using QThread::terminate(). + setCancellationEnabled(true); + data->ensureEventDispatcher(); data->eventDispatcher.loadRelaxed()->startingUp(); -#if (defined(Q_OS_LINUX) || defined(Q_OS_DARWIN) || defined(Q_OS_QNX)) - { - // Sets the name of the current thread. We can only do this - // when the thread is starting, as we don't have a cross - // platform way of setting the name of an arbitrary thread. - if (Q_LIKELY(thr->d_func()->objectName.isEmpty())) - setCurrentThreadName(thr->metaObject()->className()); - else - setCurrentThreadName(std::exchange(thr->d_func()->objectName, {}).toLocal8Bit()); - } -#endif - emit thr->started(QThread::QPrivateSignal()); -#ifdef PTHREAD_CANCEL_DISABLE - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); - pthread_testcancel(); -#endif + thr->run(); - }); + } // This calls finish(); later, the currentThreadCleanup thread-local // destructor will call cleanup(). @@ -447,26 +472,22 @@ void *QThreadPrivate::start(void *arg) void QThreadPrivate::finish() { - terminate_on_exception([&] { - QThreadPrivate *d = this; - QThread *thr = q_func(); + QThreadPrivate *d = this; + QThread *thr = q_func(); - // Disable cancellation; we're already in the finishing touches of this - // thread, and we don't want cleanup to be disturbed by - // abi::__forced_unwind being thrown from all kinds of functions. -#ifdef PTHREAD_CANCEL_DISABLE - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr); -#endif + // Disable cancellation; we're already in the finishing touches of this + // thread, and we don't want cleanup to be disturbed by + // abi::__forced_unwind being thrown from all kinds of functions. + setCancellationEnabled(false); - QMutexLocker locker(&d->mutex); + QMutexLocker locker(&d->mutex); - d->threadState = QThreadPrivate::Finishing; - locker.unlock(); - emit thr->finished(QThread::QPrivateSignal()); - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + d->threadState = QThreadPrivate::Finishing; + locker.unlock(); + emit thr->finished(QThread::QPrivateSignal()); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - QThreadStoragePrivate::finish(&d->data->tls); - }); + QThreadStoragePrivate::finish(&d->data->tls); if constexpr (QT_CONFIG(broken_threadlocal_dtors)) cleanup(); @@ -474,31 +495,27 @@ void QThreadPrivate::finish() void QThreadPrivate::cleanup() { - terminate_on_exception([&] { - QThreadPrivate *d = this; + QThreadPrivate *d = this; - // Disable cancellation again: we did it above, but some user code - // running between finish() and cleanup() may have turned them back on. -#ifdef PTHREAD_CANCEL_DISABLE - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr); -#endif + // Disable cancellation again: we did it above, but some user code + // running between finish() and cleanup() may have turned them back on. + setCancellationEnabled(false); - QMutexLocker locker(&d->mutex); - d->priority = QThread::InheritPriority; + QMutexLocker locker(&d->mutex); + d->priority = QThread::InheritPriority; - QAbstractEventDispatcher *eventDispatcher = d->data->eventDispatcher.loadRelaxed(); - if (eventDispatcher) { - d->data->eventDispatcher = nullptr; - locker.unlock(); - eventDispatcher->closingDown(); - delete eventDispatcher; - locker.relock(); - } + QAbstractEventDispatcher *eventDispatcher = d->data->eventDispatcher.loadRelaxed(); + if (eventDispatcher) { + d->data->eventDispatcher = nullptr; + locker.unlock(); + eventDispatcher->closingDown(); + delete eventDispatcher; + locker.relock(); + } - d->interruptionRequested.store(false, std::memory_order_relaxed); + d->interruptionRequested.store(false, std::memory_order_relaxed); - d->wakeAll(); - }); + d->wakeAll(); } @@ -770,10 +787,14 @@ void QThread::start(Priority priority) pthread_attr_init(&attr); if constexpr (!UsingPThreadTimedJoin) pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (d->serviceLevel != QThread::QualityOfService::Auto) { #ifdef Q_OS_DARWIN - if (d->serviceLevel != QThread::QualityOfService::Auto) pthread_attr_set_qos_class_np(&attr, d->nativeQualityOfServiceClass(), 0); +#else + // No such functionality on other OSes. We promise "no effect", so don't + // print a warning either. #endif + } d->priority = priority; @@ -995,13 +1016,7 @@ void QThread::setTerminationEnabled(bool enabled) "Current thread was not started with QThread."); Q_UNUSED(thr); -#if defined(Q_OS_ANDROID) - Q_UNUSED(enabled); -#else - pthread_setcancelstate(enabled ? PTHREAD_CANCEL_ENABLE : PTHREAD_CANCEL_DISABLE, nullptr); - if (enabled) - pthread_testcancel(); -#endif + setCancellationEnabled(enabled); } // Caller must lock the mutex diff --git a/src/corelib/thread/qthread_win.cpp b/src/corelib/thread/qthread_win.cpp index f53e488a8d7..f5a7756f2b2 100644 --- a/src/corelib/thread/qthread_win.cpp +++ b/src/corelib/thread/qthread_win.cpp @@ -145,13 +145,14 @@ unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(voi { QThread *thr = reinterpret_cast<QThread *>(arg); QThreadData *data = QThreadData::get2(thr); - // If a QThread is restarted, reuse the QBindingStatus, too - data->reuseBindingStatusForNewNativeThread(); data->ref(); set_thread_data(data); data->threadId.storeRelaxed(QThread::currentThreadId()); + // If a QThread is restarted, reuse the QBindingStatus, too + data->reuseBindingStatusForNewNativeThread(); + QThread::setTerminationEnabled(false); { diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp index c0b07ddd652..62afd172d57 100644 --- a/src/network/access/qhttp2connection.cpp +++ b/src/network/access/qhttp2connection.cpp @@ -15,6 +15,7 @@ #include <algorithm> #include <memory> +#include <chrono> QT_BEGIN_NAMESPACE @@ -646,6 +647,8 @@ void QHttp2Stream::setState(State newState) streamID(), int(m_state), int(newState)); m_state = newState; emit stateChanged(newState); + if (m_state == State::Closed) + getConnection()->maybeCloseOnGoingAway(); } // Changes the state as appropriate given the current state and the transition. @@ -1033,6 +1036,27 @@ QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const return m_streams.value(streamID, nullptr).get(); } +/*! + Initiates connection shutdown. When \a errorCode is \c{NO_ERROR}, graceful + shutdown is initiated, allowing existing streams to complete. Otherwise the + connection is closed immediately with an error. +*/ +void QHttp2Connection::close(Http2::Http2Error errorCode) +{ + if (m_connectionAborted) + return; + + if (errorCode == Http2::HTTP2_NO_ERROR) { + if (m_connectionType == Type::Server) + sendInitialServerGracefulShutdownGoaway(); + else + sendClientGracefulShutdownGoaway(); + } else { + // RFC 9113, 5.4.1: After sending the GOAWAY frame for an error + // condition, the endpoint MUST close the TCP connection + connectionError(errorCode, "Connection closed with error", false); + } +} /*! \fn QHttp2Stream *QHttp2Connection::promisedStream(const QUrl &streamKey) const @@ -1042,13 +1066,6 @@ QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const */ /*! - \fn void QHttp2Connection::close() - - This sends a GOAWAY frame on the connection stream, gracefully closing the - connection. -*/ - -/*! \fn bool QHttp2Connection::isGoingAway() const noexcept Returns \c true if the connection is in the process of being closed, or @@ -1154,13 +1171,6 @@ void QHttp2Connection::handleReadyRead() if (m_connectionType == Type::Server && !serverCheckClientPreface()) return; - const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) { - return stream && stream->isActive(); - }; - if (m_goingAway && std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) { - close(); - return; - } QIODevice *socket = getSocket(); qCDebug(qHttp2ConnectionLog, "[%p] Receiving data, %lld bytes available", this, @@ -1170,13 +1180,13 @@ void QHttp2Connection::handleReadyRead() if (!m_prefaceSent) return; - while (!m_goingAway || std::any_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) { + while (!m_connectionAborted) { const auto result = frameReader.read(*socket); if (result != FrameStatus::goodFrame) qCDebug(qHttp2ConnectionLog, "[%p] Tried to read frame, got %d", this, int(result)); switch (result) { case FrameStatus::incompleteFrame: - return; + return; // No more complete frames available case FrameStatus::protocolError: return connectionError(PROTOCOL_ERROR, "invalid frame"); case FrameStatus::sizeError: { @@ -1297,24 +1307,35 @@ void QHttp2Connection::setH2Configuration(QHttp2Configuration config) encoder.setCompressStrings(m_config.huffmanCompressionEnabled()); } -void QHttp2Connection::connectionError(Http2Error errorCode, const char *message) +void QHttp2Connection::connectionError(Http2Error errorCode, const char *message, bool logAsError) { Q_ASSERT(message); - // RFC 9113, 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change. + if (m_connectionAborted) + return; + m_connectionAborted = true; - qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message, - int(errorCode)); + if (logAsError) { + qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message, + int(errorCode)); + } else { + qCDebug(qHttp2ConnectionLog, "[%p] Closing connection: %s (%d)", this, message, + int(errorCode)); + } - // RFC 9113, 6.8: Endpoints SHOULD always send a GOAWAY frame before closing a connection so - // that the remote peer can know whether a stream has been partially processed or not. - sendGOAWAY(errorCode); + // Mark going away so other code paths will stop creating new streams + m_goingAway = true; + // RFC 9113 5.4.1: An endpoint that encounters a connection error SHOULD + // first send a GOAWAY frame with the last incoming stream ID. + m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess); + sendGOAWAYFrame(errorCode, m_lastStreamToProcess); auto messageView = QLatin1StringView(message); for (QHttp2Stream *stream : std::as_const(m_streams)) { if (stream && stream->isActive()) stream->finishWithError(errorCode, messageView); } - + // RFC 9113 5.4.1: After sending the GOAWAY frame for an error condition, + // the endpoint MUST close the TCP connection closeSession(); } @@ -1423,20 +1444,99 @@ bool QHttp2Connection::sendWINDOW_UPDATE(quint32 streamID, quint32 delta) return frameWriter.write(*getSocket()); } -bool QHttp2Connection::sendGOAWAY(Http2::Http2Error errorCode) +void QHttp2Connection::sendClientGracefulShutdownGoaway() { + // Clients send a single GOAWAY. No race condition since they control stream creation + Q_ASSERT(m_connectionType == Type::Client); + + if (m_connectionAborted || m_goingAway) { + qCWarning(qHttp2ConnectionLog, "[%p] Client graceful shutdown already in progress", this); + return; + } + m_goingAway = true; - // If this is the first time, start the timer: - if (m_lastStreamToProcess == Http2::lastValidStreamID) - m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod); - m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess); + m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent; + m_lastStreamToProcess = m_lastIncomingStreamID; + sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess); + + maybeCloseOnGoingAway(); +} + +void QHttp2Connection::sendInitialServerGracefulShutdownGoaway() +{ + Q_ASSERT(m_connectionType == Type::Server); + // RFC 9113, 6.8: A server that is attempting to gracefully shut down a + // connection SHOULD send an initial GOAWAY frame with the last stream + // identifier set to 2^31-1 and a NO_ERROR code. + if (m_connectionAborted || m_goingAway) { + qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown already in progress", this); + return; + } + + m_goingAway = true; + m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod); + sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, Http2::lastValidStreamID); + + // Send PING to measure RTT; handlePING() continues the shutdown on ACK. + // RFC 9113 6.8: After allowing time for any in-flight stream creation + // (at least one round-trip time) + if (sendPing()) + m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing; + else + m_gracefulShutdownState = GracefulShutdownState::AwaitingPriorPing; +} + +void QHttp2Connection::sendFinalServerGracefulShutdownGoaway() +{ + if (m_connectionAborted || !m_goingAway) { + qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown not in progress", this); + return; + } + m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent; + m_lastStreamToProcess = m_lastIncomingStreamID; + sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess); + maybeCloseOnGoingAway(); +} + +bool QHttp2Connection::sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastStreamID) +{ + QIODevice *socket = getSocket(); + if (!socket || !socket->isOpen()) + return false; + qCDebug(qHttp2ConnectionLog, "[%p] Sending GOAWAY frame, error code %u, last stream %u", this, - errorCode, m_lastStreamToProcess); + errorCode, lastStreamID); + frameWriter.start(FrameType::GOAWAY, FrameFlag::EMPTY, Http2PredefinedParameters::connectionStreamID); - frameWriter.append(m_lastStreamToProcess); + frameWriter.append(lastStreamID); frameWriter.append(quint32(errorCode)); - return frameWriter.write(*getSocket()); + return frameWriter.write(*socket); +} + +void QHttp2Connection::maybeCloseOnGoingAway() +{ + // Only close if we've reached the final phase of graceful shutdown + // For the sender: after FinalGOAWAYSent + // For the receiver: after receiving GOAWAY and all our streams are done + if (m_connectionAborted || !m_goingAway) { + qCDebug(qHttp2ConnectionLog, "[%p] Connection close deferred, graceful shutdown not active", + this); + return; + } + + // For graceful shutdown initiator, only close after final GOAWAY is sent + if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing) + return; // Still waiting for RTT measurement before final GOAWAY + + const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) { + return stream && stream->isActive(); + }; + + if (std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) { + qCDebug(qHttp2ConnectionLog, "[%p] All streams closed, closing connection", this); + closeSession(); + } } bool QHttp2Connection::sendSETTINGS_ACK() @@ -1571,8 +1671,6 @@ void QHttp2Connection::handleHEADERS() qCDebug(qHttp2ConnectionLog, "[%p] HEADERS frame on stream %d has PRIORITY flag", this, streamID); handlePRIORITY(); - if (m_goingAway) - return; } const bool endHeaders = flags.testFlag(FrameFlag::END_HEADERS); @@ -1834,6 +1932,17 @@ void QHttp2Connection::handlePING() emit pingFrameReceived(PingState::PongSignatureIdentical); } m_lastPingSignature.reset(); + + // Handle sendInitialServerGracefulShutdownGoaway() + if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing) { + sendFinalServerGracefulShutdownGoaway(); + } else if (m_gracefulShutdownState == GracefulShutdownState::AwaitingPriorPing) { + // Prior PING completed, now send our RTT measurement PING. This shouldn't fail! + m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing; + [[maybe_unused]] const bool ok = sendPing(); + Q_ASSERT(ok); + } + return; } else { emit pingFrameReceived(PingState::Ping); @@ -1876,27 +1985,44 @@ void QHttp2Connection::handleGOAWAY() if (lastStreamID != 0 && (lastStreamID & 0x1) != LocalMask) return connectionError(PROTOCOL_ERROR, "GOAWAY with invalid last stream ID"); + // 6.8 - An endpoint MAY send multiple GOAWAY frames if circumstances + // change. Endpoints MUST NOT increase the value they send in the last + // stream identifier + if (m_lastGoAwayLastStreamID && lastStreamID > *m_lastGoAwayLastStreamID) + return connectionError(PROTOCOL_ERROR, "Repeated GOAWAY with invalid last stream ID"); + m_lastGoAwayLastStreamID = lastStreamID; + qCDebug(qHttp2ConnectionLog, "[%p] Received GOAWAY frame, error code %u, last stream %u", this, errorCode, lastStreamID); m_goingAway = true; emit receivedGOAWAY(errorCode, lastStreamID); - // Since the embedded stream ID is the last one that was or _might be_ processed, - // we cancel anything that comes after it. 0 can be used in the special case that - // no streams at all were or will be processed. - const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2; - const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream; - Q_ASSERT((firstCancelledStream & 0x1) == LocalMask); - for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) { - QHttp2Stream *stream = m_streams.value(id, nullptr); - if (stream && stream->isActive()) - stream->finishWithError(errorCode, "Received GOAWAY"_L1); - } - - const auto isActive = [](const QHttp2Stream *stream) { return stream && stream->isActive(); }; - if (std::none_of(m_streams.cbegin(), m_streams.cend(), isActive)) + if (errorCode == HTTP2_NO_ERROR) { + // Graceful GOAWAY (NO_ERROR): Only cancel streams the peer explicitly won't process + // (those with IDs > lastStreamID). Streams with ID <= lastStreamID can still complete. + // '0' can be used in the special case that no streams at all were or will be processed. + const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2; + const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream; + Q_ASSERT((firstCancelledStream & 0x1) == LocalMask); + for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) { + QHttp2Stream *stream = m_streams.value(id, nullptr); + if (stream && stream->isActive()) + stream->finishWithError(errorCode, "Received GOAWAY"_L1); + } + maybeCloseOnGoingAway(); // check if we can close now + } else { + // RFC 9113, 5.4.1: After sending the GOAWAY frame for an error + // condition, the endpoint MUST close the TCP connection. + // As the peer is closing the connection immediately, they won't + // process any more data, so we close the connection here already. + m_connectionAborted = true; + for (QHttp2Stream *stream : std::as_const(m_streams)) { + if (stream && stream->isActive()) + stream->finishWithError(errorCode, "Received GOAWAY"_L1); + } closeSession(); + } } void QHttp2Connection::handleWINDOW_UPDATE() diff --git a/src/network/access/qhttp2connection_p.h b/src/network/access/qhttp2connection_p.h index e2af7d7ab33..d9f2bccc58a 100644 --- a/src/network/access/qhttp2connection_p.h +++ b/src/network/access/qhttp2connection_p.h @@ -260,7 +260,7 @@ public: return nullptr; } - void close(Http2::Http2Error error = Http2::HTTP2_NO_ERROR) { sendGOAWAY(error); } + void close(Http2::Http2Error errorCode = Http2::HTTP2_NO_ERROR); bool isGoingAway() const noexcept { return m_goingAway; } @@ -302,8 +302,7 @@ private: Q_ALWAYS_INLINE bool streamIsIgnored(quint32 streamID) const noexcept; - void connectionError(Http2::Http2Error errorCode, - const char *message); // Connection failed to be established? + void connectionError(Http2::Http2Error errorCode, const char *message, bool logAsError = true); void setH2Configuration(QHttp2Configuration config); void closeSession(); void registerStreamAsResetLocally(quint32 streamID); @@ -316,7 +315,11 @@ private: bool sendServerPreface(); bool serverCheckClientPreface(); bool sendWINDOW_UPDATE(quint32 streamID, quint32 delta); - bool sendGOAWAY(Http2::Http2Error errorCode); + void sendClientGracefulShutdownGoaway(); + void sendInitialServerGracefulShutdownGoaway(); + void sendFinalServerGracefulShutdownGoaway(); + bool sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastSreamID); + void maybeCloseOnGoingAway(); bool sendSETTINGS_ACK(); void handleDATA(); @@ -423,6 +426,17 @@ private: static constexpr std::chrono::duration GoawayGracePeriod = std::chrono::seconds(60); QDeadlineTimer m_goawayGraceTimer; + std::optional<quint32> m_lastGoAwayLastStreamID; + bool m_connectionAborted = false; + + enum class GracefulShutdownState { + None, + AwaitingPriorPing, + AwaitingShutdownPing, + FinalGOAWAYSent, + }; + GracefulShutdownState m_gracefulShutdownState = GracefulShutdownState::None; + bool m_prefaceSent = false; // Server-side only: diff --git a/src/tools/syncqt/main.cpp b/src/tools/syncqt/main.cpp index cfef5be7e2f..3ec70bcdfcd 100644 --- a/src/tools/syncqt/main.cpp +++ b/src/tools/syncqt/main.cpp @@ -1673,7 +1673,7 @@ public: void SyncScanner::updateSymbolDescriptor(const std::string &symbol, const std::string &file, SymbolDescriptor::SourceType type) { - if (m_commandLineArgs->showOnly()) + if (m_commandLineArgs->showOnly() || m_commandLineArgs->debug()) std::cout << " SYMBOL: " << symbol << std::endl; m_symbols[symbol].update(file, type); } diff --git a/src/widgets/widgets/qtabbar.cpp b/src/widgets/widgets/qtabbar.cpp index 44218d41ded..84fe8c3f44a 100644 --- a/src/widgets/widgets/qtabbar.cpp +++ b/src/widgets/widgets/qtabbar.cpp @@ -834,7 +834,7 @@ void QTabBarPrivate::scrollTabs() const auto &tabRect = tab->rect; int start = horizontal ? tabRect.left() : tabRect.top(); int end = horizontal ? tabRect.right() : tabRect.bottom(); - if (end > scrollRect.right() && start > scrollOffset) { + if (end > scrollRect.right() && start > scrollRect.left()) { makeVisible(i); return; } @@ -2449,10 +2449,12 @@ void QTabBar::wheelEvent(QWheelEvent *event) if (!d->rightB->isVisible()) scrollRectExtent += tabsVertical ? d->rightB->height() : d->rightB->width(); + const QRect scrollRect0 = d->normalizedScrollRect(0); + const int minScrollOffset = -1 * scrollRect0.left(); const int maxScrollOffset = qMax((tabsVertical ? lastTabRect.bottom() : lastTabRect.right()) - scrollRectExtent, 0); - d->scrollOffset = qBound(0, d->scrollOffset - delta, maxScrollOffset); + d->scrollOffset = qBound(minScrollOffset, d->scrollOffset - delta, maxScrollOffset); d->leftB->setEnabled(d->scrollOffset > -scrollRect.left()); d->rightB->setEnabled(maxScrollOffset > d->scrollOffset); if (oldScrollOffset != d->scrollOffset) { |
