summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/corelib/Qt6AndroidGradleHelpers.cmake4
-rw-r--r--src/corelib/Qt6AndroidMacros.cmake3
-rw-r--r--src/corelib/kernel/qcoreapplication.cpp16
-rw-r--r--src/corelib/thread/qthread_p.h10
-rw-r--r--src/corelib/thread/qthread_unix.cpp219
-rw-r--r--src/corelib/thread/qthread_win.cpp5
-rw-r--r--src/network/access/qhttp2connection.cpp222
-rw-r--r--src/network/access/qhttp2connection_p.h22
-rw-r--r--src/tools/syncqt/main.cpp2
-rw-r--r--src/widgets/widgets/qtabbar.cpp6
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) {