diff options
author | Friedemann Kleint <[email protected]> | 2024-12-10 09:57:12 +0100 |
---|---|---|
committer | Friedemann Kleint <[email protected]> | 2024-12-16 19:38:28 +0100 |
commit | cab304e70cce68bbdaa70d7f7b2bf6e95e85e6d2 (patch) | |
tree | b35f9a49401b8e4d9347dce05e36b7e02ed74cef | |
parent | e226b5827c110d16c9d3b3f1b8d62b1c3a6e8605 (diff) |
libpyside/ PySideSignal: Fix a memory leak connecting to signals
Change dc7acd1f2dc750c3c8602203ae1558b0e60a3c17 added a reference
to signal senders not created in Python to fix a crash when doing
something like:
QAbstractItemView.selectionModel().currentChanged.connect(...)
In addition, the code kept a weakref on the sender and tracked its
deletion.
To simplify this, keep a tracking QPointer on the sender QObject and
its PyTypeObject * instead of a PyObject * . This also allows for
calling QObject::connect() and other helpers directly instead of using
PyObject_CallObject() on the PyObject * to forward the calls.
Fixes: PYSIDE-2793
Fixes: PYSIDE-1057
Task-number: PYSIDE-79
Change-Id: I1ce6f4c35c819f3e815161788cdef964ffc6fd96
Reviewed-by: Christian Tismer <[email protected]>
Reviewed-by: Shyamnath Premnadh <[email protected]>
-rw-r--r-- | sources/pyside6/libpyside/pyside.cpp | 2 | ||||
-rw-r--r-- | sources/pyside6/libpyside/pysidesignal.cpp | 279 | ||||
-rw-r--r-- | sources/pyside6/libpyside/pysidesignal.h | 3 | ||||
-rw-r--r-- | sources/pyside6/libpyside/pysidesignal_p.h | 6 | ||||
-rw-r--r-- | sources/pyside6/tests/QtCore/destroysignal_test.py | 2 |
5 files changed, 157 insertions, 135 deletions
diff --git a/sources/pyside6/libpyside/pyside.cpp b/sources/pyside6/libpyside/pyside.cpp index 02cff066b..f3d20a023 100644 --- a/sources/pyside6/libpyside/pyside.cpp +++ b/sources/pyside6/libpyside/pyside.cpp @@ -660,7 +660,7 @@ PyObject *getHiddenDataFromQObject(QObject *cppSelf, PyObject *self, PyObject *n } if (!signalList.isEmpty()) { auto *pySignal = reinterpret_cast<PyObject *>( - Signal::newObjectFromMethod(self, signalList)); + Signal::newObjectFromMethod(cppSelf, self, signalList)); PyObject_SetAttr(self, name, pySignal); return pySignal; } diff --git a/sources/pyside6/libpyside/pysidesignal.cpp b/sources/pyside6/libpyside/pysidesignal.cpp index 959f5a6d8..887db917a 100644 --- a/sources/pyside6/libpyside/pysidesignal.cpp +++ b/sources/pyside6/libpyside/pysidesignal.cpp @@ -7,7 +7,7 @@ #include "pysideqobject.h" #include "pysideutils.h" #include "pysidestaticstrings.h" -#include "pysideweakref.h" +#include "qobjectconnect.h" #include "signalmanager.h" #include <shiboken.h> @@ -22,6 +22,7 @@ #include <pep384ext.h> #include <signature.h> #include <sbkenum.h> +#include <sbkconverter.h> #include <algorithm> #include <optional> @@ -74,7 +75,17 @@ QDebug operator<<(QDebug debug, const PySideSignalInstancePrivate &d) static bool isSourceDeleted(const PySideSignalInstance *p) { - return p->d == nullptr || p->d->shared->deleted; + return p->d == nullptr || p->d->shared->source.isNull(); +} + +static inline QObject *sender(const PySideSignalInstance *p) +{ + return p->d != nullptr ? p->d->shared->source.data() : nullptr; +} + +static inline QByteArray qSignalSignature(const PySideSignalInstance *p) +{ + return QT_SIGNAL_SENTINEL + p->d->signature; } static bool connection_Check(PyObject *o) @@ -110,12 +121,21 @@ static std::optional<QByteArrayList> parseArgumentNames(PyObject *argArguments) return result; } +static const char msgSourceDeleted[] = "Signal source has been deleted"; +static const char msgTargetSignalDeleted[] = "Target signal has been deleted"; + +static SbkConverter *metaObjConnectionConverter() +{ + static SbkConverter *result = Shiboken::Conversions::getConverter("QMetaObject::Connection"); + Q_ASSERT(result); + return result; +} + namespace PySide::Signal { static QByteArray buildSignature(const QByteArray &, const QByteArray &); static void instanceInitialize(PySideSignalInstance *, PyObject *, PySideSignal *, const PySideSignalInstanceSharedPtr &shared, int); static PySideSignalData::Signature parseSignature(PyObject *); - static PyObject *buildQtCompatible(const QByteArray &); } // PySide::Signal extern "C" @@ -373,7 +393,6 @@ static void signalInstanceFree(void *vself) Py_DECREF(dataPvt->next); dataPvt->next = nullptr; } - dataPvt->shared->deleted = true; delete dataPvt; self->d = nullptr; } @@ -517,68 +536,56 @@ static PyObject *signalInstanceConnect(PyObject *self, PyObject *args, PyObject "O|O:SignalInstance", const_cast<char **>(kwlist), &slot, &type)) return nullptr; + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (type != nullptr && qstrcmp(Py_TYPE(type)->tp_name, "ConnectionType") == 0) { + static SbkConverter *connectionTypeConv = + Shiboken::Conversions::getConverter("Qt::ConnectionType"); + Q_ASSERT(connectionTypeConv); + Shiboken::Conversions::pythonToCppCopy(connectionTypeConv, type, &connectionType); + } + auto *source = reinterpret_cast<PySideSignalInstance *>(self); if (!source->d) return PyErr_Format(PyExc_RuntimeError, "cannot connect uninitialized SignalInstance"); - if (isSourceDeleted(source)) - return PyErr_Format(PyExc_RuntimeError, "Signal source has been deleted"); - - Shiboken::AutoDecRef pyArgs(PyList_New(0)); - bool match = false; - if (Py_TYPE(slot) == PySideSignalInstance_TypeF()) { - PySideSignalInstance *sourceWalk = source; - - //find best match - while (sourceWalk && !match) { - auto *targetWalk = reinterpret_cast<PySideSignalInstance *>(slot); - while (targetWalk && !match) { + if (Py_TYPE(slot) == PySideSignalInstance_TypeF()) { // Connect signal to signal + // find best match + for (PySideSignalInstance *sourceWalk = source; sourceWalk != nullptr; ) { + for (auto *targetWalk = reinterpret_cast<PySideSignalInstance *>(slot); + targetWalk != nullptr; ) { if (QMetaObject::checkConnectArgs(sourceWalk->d->signature, targetWalk->d->signature)) { - PyList_Append(pyArgs, sourceWalk->d->shared->source); - Shiboken::AutoDecRef sourceSignature(PySide::Signal::buildQtCompatible(sourceWalk->d->signature)); - PyList_Append(pyArgs, sourceSignature); - - PyList_Append(pyArgs, targetWalk->d->shared->source); - Shiboken::AutoDecRef targetSignature(PySide::Signal::buildQtCompatible(targetWalk->d->signature)); - PyList_Append(pyArgs, targetSignature); - - match = true; + if (isSourceDeleted(sourceWalk)) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); + if (isSourceDeleted(targetWalk)) + return PyErr_Format(PyExc_RuntimeError, msgTargetSignalDeleted); + + auto conn = PySide::qobjectConnect(sender(sourceWalk), + qSignalSignature(sourceWalk).constData(), + sender(targetWalk), + qSignalSignature(targetWalk).constData(), + connectionType); + return Shiboken::Conversions::copyToPython(metaObjConnectionConverter(), &conn); } targetWalk = reinterpret_cast<PySideSignalInstance *>(targetWalk->d->next); } sourceWalk = reinterpret_cast<PySideSignalInstance *>(sourceWalk->d->next); } - } else { - // Adding references to pyArgs - PyList_Append(pyArgs, source->d->shared->source); - - // Check signature of the slot (method or function) to match signal - PySideSignalInstance *matchedSlot = findSignalInstanceForSlot(source, slot); - Shiboken::AutoDecRef signature(PySide::Signal::buildQtCompatible(matchedSlot->d->signature)); - PyList_Append(pyArgs, signature); - PyList_Append(pyArgs, slot); - match = true; + return PyErr_Format(PyExc_RuntimeError, "Failed to connect signal %s.", + source->d->signature.constData()); } - if (type) - PyList_Append(pyArgs, type); - - if (match) { - Shiboken::AutoDecRef tupleArgs(PyList_AsTuple(pyArgs)); - Shiboken::AutoDecRef pyMethod(PyObject_GetAttr(source->d->shared->source, - PySide::PySideName::qtConnect())); - if (pyMethod.isNull()) // PYSIDE-79: check if pyMethod exists. - return PyErr_Format(PyExc_RuntimeError, "method 'connect' vanished!"); - PyObject *result = PyObject_CallObject(pyMethod, tupleArgs); - if (connection_Check(result)) - return result; - Py_XDECREF(result); + if (PyCallable_Check(slot) == 0) { + return PyErr_Format(PyExc_TypeError, "Expected signal or callable, got \"%s\"", + Py_TYPE(slot)->tp_name); } - if (!PyErr_Occurred()) // PYSIDE-79: inverse the logic. A Null return needs an error. - PyErr_Format(PyExc_RuntimeError, "Failed to connect signal %s.", - source->d->signature.constData()); - return nullptr; + if (isSourceDeleted(source)) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); + PySideSignalInstance *matchedSlot = findSignalInstanceForSlot(source, slot); + auto conn = PySide::qobjectConnectCallback(sender(source), + qSignalSignature(matchedSlot).constData(), + slot, connectionType); + return Shiboken::Conversions::copyToPython(metaObjConnectionConverter(), &conn); } static int argCountInSignature(const char *signature) @@ -592,10 +599,8 @@ static PyObject *signalInstanceEmit(PyObject *self, PyObject *args) if (!source->d) return PyErr_Format(PyExc_RuntimeError, "cannot emit uninitialized SignalInstance"); - // PYSIDE-2201: Check if the object has vanished meanwhile. - // Tried to revive it without exception, but this gives problems. if (isSourceDeleted(source)) - return PyErr_Format(PyExc_RuntimeError, "The SignalInstance object was already deleted"); + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); Shiboken::AutoDecRef pyArgs(PyList_New(0)); Py_ssize_t numArgsGiven = PySequence_Size(args); @@ -619,17 +624,15 @@ static PyObject *signalInstanceEmit(PyObject *self, PyObject *args) } } } - Shiboken::AutoDecRef sourceSignature(PySide::Signal::buildQtCompatible(source->d->signature)); - - PyList_Append(pyArgs, sourceSignature); - for (Py_ssize_t i = 0, max = PyTuple_Size(args); i < max; i++) - PyList_Append(pyArgs, PyTuple_GetItem(args, i)); - - Shiboken::AutoDecRef pyMethod(PyObject_GetAttr(source->d->shared->source, - PySide::PySideName::qtEmit())); - Shiboken::AutoDecRef tupleArgs(PyList_AsTuple(pyArgs)); - return PyObject_CallObject(pyMethod.object(), tupleArgs); + const bool ok = PySide::SignalManager::emitSignal(sender(source), + qSignalSignature(source).constData(), + args); + if (PyErr_Occurred() != nullptr) + return nullptr; + if (ok) + Py_RETURN_TRUE; + Py_RETURN_FALSE; } static PyObject *signalInstanceGetItem(PyObject *self, PyObject *key) @@ -686,46 +689,74 @@ static PyObject *signalInstanceDisconnect(PyObject *self, PyObject *args) if (PyTuple_Check(args) && PyTuple_Size(args)) slot = PyTuple_GetItem(args, 0); - bool match = false; - if (Py_TYPE(slot) == PySideSignalInstance_TypeF()) { + if (Py_TYPE(slot) == PySideSignalInstance_TypeF()) { // Disconnect signal from signal auto *target = reinterpret_cast<PySideSignalInstance *>(slot); - if (QMetaObject::checkConnectArgs(source->d->signature, target->d->signature)) { - PyList_Append(pyArgs, source->d->shared->source); - Shiboken::AutoDecRef source_signature(PySide::Signal::buildQtCompatible(source->d->signature)); - PyList_Append(pyArgs, source_signature); - - PyList_Append(pyArgs, target->d->shared->source); - Shiboken::AutoDecRef target_signature(PySide::Signal::buildQtCompatible(target->d->signature)); - PyList_Append(pyArgs, target_signature); - match = true; + if (!QMetaObject::checkConnectArgs(source->d->signature, target->d->signature)) { + warnDisconnectFailed(slot, source->d->signature); + Py_RETURN_FALSE; } - } else if (connection_Check(slot)) { - PyList_Append(pyArgs, slot); - match = true; - } else { - // try the matching signature, fall back to first - auto *matchedSlot = slot != Py_None ? findSignalInstanceForSlot(source, slot) : source; - PyList_Append(pyArgs, matchedSlot->d->shared->source); - Shiboken::AutoDecRef signature(PySide::Signal::buildQtCompatible(matchedSlot->d->signature)); - PyList_Append(pyArgs, signature); - - // disconnect all, so we need to use the c++ signature disconnect(qobj, signal, 0, 0) - if (slot == Py_None) - PyList_Append(pyArgs, slot); - PyList_Append(pyArgs, slot); - match = true; + + if (isSourceDeleted(source)) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); + if (isSourceDeleted(target)) + return PyErr_Format(PyExc_RuntimeError, msgTargetSignalDeleted); + + auto *qSender = sender(source); + const bool ok = qSender->disconnect(qSignalSignature(source).constData(), + sender(target), + qSignalSignature(target).constData()); + if (ok) + Py_RETURN_TRUE; + warnDisconnectFailed(slot, source->d->signature); + Py_RETURN_FALSE; } - if (match) { - Shiboken::AutoDecRef tupleArgs(PyList_AsTuple(pyArgs)); - Shiboken::AutoDecRef pyMethod(PyObject_GetAttr(source->d->shared->source, - PySide::PySideName::qtDisconnect())); - PyObject *result = PyObject_CallObject(pyMethod, tupleArgs); - if (result != Py_True) - warnDisconnectFailed(slot, source->d->signature); - return result; + if (slot == Py_None) { // disconnect all signal receivers by signature + if (isSourceDeleted(source)) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); + const bool ok = + source->d->shared->source->disconnect(qSignalSignature(source).constData()); + if (ok) + Py_RETURN_TRUE; + warnDisconnectFailed(slot, source->d->signature); + Py_RETURN_FALSE; + } + + if (connection_Check(slot)) { // disconnection by connection ID + QMetaObject::Connection conn; + Shiboken::Conversions::pythonToCppCopy(metaObjConnectionConverter(), slot, &conn); + if (conn && QObject::disconnect(conn)) + Py_RETURN_TRUE; + warnDisconnectFailed(slot, source->d->signature); + Py_RETURN_FALSE; + } + + // Disconnect callable + if (PyCallable_Check(slot) == 0) { + return PyErr_Format(PyExc_TypeError, + "Expected signal, callable or connection id, got \"%s\"", + Py_TYPE(slot)->tp_name); } + // try the matching signature, fall back to first + auto *matchedSlot = slot != Py_None ? findSignalInstanceForSlot(source, slot) : source; + if (isSourceDeleted(matchedSlot)) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); + + const auto sourceSignature = qSignalSignature(matchedSlot); + // disconnect all, so we need to use the c++ signature disconnect(qobj, signal, 0, 0) + auto *qSender = sender(matchedSlot); + if (slot == Py_None) { + const bool ok = qSender->disconnect(sourceSignature.constData(), nullptr, nullptr); + if (ok) + Py_RETURN_TRUE; + warnDisconnectFailed(slot, source->d->signature); + Py_RETURN_FALSE; + } + + const bool ok = PySide::qobjectDisconnectCallback(qSender, sourceSignature, slot); + if (ok) + Py_RETURN_TRUE; warnDisconnectFailed(slot, source->d->signature); Py_RETURN_FALSE; } @@ -807,7 +838,7 @@ static PyObject *_getHomonymousMethod(PySideSignalInstance *inst) // but walk through the whole mro to find a hidden method with the same name. auto signalName = inst->d->signalName; Shiboken::AutoDecRef name(Shiboken::String::fromCString(signalName)); - auto *mro = Py_TYPE(inst->d->shared->source)->tp_mro; + auto *mro = inst->d->shared->sourceType->tp_mro; const Py_ssize_t n = PyTuple_Size(mro); for (Py_ssize_t idx = 0; idx < n; idx++) { @@ -825,6 +856,8 @@ static PyObject *_getHomonymousMethod(PySideSignalInstance *inst) static PyObject *signalInstanceCall(PyObject *self, PyObject *args, PyObject *kw) { auto *PySideSignal = reinterpret_cast<PySideSignalInstance *>(self); + if (isSourceDeleted(PySideSignal)) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); auto *hom = _getHomonymousMethod(PySideSignal); if (!hom) { PyErr_Format(PyExc_TypeError, "native Qt signal instance '%s' is not callable", @@ -832,8 +865,11 @@ static PyObject *signalInstanceCall(PyObject *self, PyObject *args, PyObject *kw return nullptr; } - Shiboken::AutoDecRef homonymousMethod(PepExt_Type_CallDescrGet(hom, PySideSignal->d->shared->source, - nullptr)); + auto *source = PySide::getWrapperForQObject(sender(PySideSignal), + PySideSignal->d->shared->sourceType); + if (source == nullptr) + return PyErr_Format(PyExc_RuntimeError, msgSourceDeleted); + Shiboken::AutoDecRef homonymousMethod(PepExt_Type_CallDescrGet(hom, source, nullptr)); return PyObject_Call(homonymousMethod, args, kw); } @@ -935,7 +971,8 @@ void updateSourceObject(PyObject *source) Shiboken::AutoDecRef signalInstance(reinterpret_cast<PyObject *>(inst)); auto *si = reinterpret_cast<PySideSignalInstance *>(signalInstance.object()); auto shared = std::make_shared<PySideSignalInstanceShared>(); - shared->source = source; + shared->source = PySide::convertToQObject(source, false); + shared->sourceType = Py_TYPE(source); instanceInitialize(si, key, reinterpret_cast<PySideSignal *>(value), shared, 0); if (PyDict_SetItem(dict, key, signalInstance) == -1) @@ -1026,13 +1063,6 @@ static PySideSignalData::Signature parseSignature(PyObject *args) return result; } -static void sourceGone(void *data) -{ - auto *ptr = reinterpret_cast<PySideSignalInstanceSharedPtr *>(data); - (*ptr)->deleted = true; - delete ptr; -} - static void instanceInitialize(PySideSignalInstance *self, PyObject *name, PySideSignal *signal, const PySideSignalInstanceSharedPtr &shared, @@ -1041,13 +1071,6 @@ static void instanceInitialize(PySideSignalInstance *self, PyObject *name, self->d = new PySideSignalInstancePrivate; PySideSignalInstancePrivate *selfPvt = self->d; selfPvt->shared = shared; - // PYSIDE-2201: We have no reference to source. Let's take a weakref to get - // notified when source gets deleted. - if (index == 0) { - auto *ptr = new PySideSignalInstanceSharedPtr(shared); - PySide::WeakRef::create(shared->source, sourceGone, ptr); - } - selfPvt->next = nullptr; if (signal->data->signalName.isEmpty()) signal->data->signalName = Shiboken::String::toCString(name); @@ -1085,11 +1108,9 @@ PySideSignalInstance *initialize(PySideSignal *self, PyObject *name, PyObject *o PySideSignalInstance *instance = PyObject_New(PySideSignalInstance, PySideSignalInstance_TypeF()); auto shared = std::make_shared<PySideSignalInstanceShared>(); - shared->source = object; + shared->source = PySide::convertToQObject(object, false); + shared->sourceType = Py_TYPE(object); instanceInitialize(instance, name, self, shared, 0); - auto *sbkObj = reinterpret_cast<SbkObject *>(object); - if (!Shiboken::Object::wasCreatedByPython(sbkObj)) - Py_INCREF(object); // PYSIDE-79: this flag was crucial for a wrapper call. return instance; } @@ -1111,12 +1132,14 @@ bool connect(PyObject *source, const char *signal, PyObject *callback) return result; } -PySideSignalInstance *newObjectFromMethod(PyObject *source, const QList<QMetaMethod>& methodList) +PySideSignalInstance *newObjectFromMethod(QObject *sourceQObject, PyObject *source, + const QList<QMetaMethod>& methodList) { PySideSignalInstance *root = nullptr; PySideSignalInstance *previous = nullptr; auto shared = std::make_shared<PySideSignalInstanceShared>(); - shared->source = source; + shared->source = sourceQObject; + shared->sourceType = Py_TYPE(source); for (const QMetaMethod &m : methodList) { PySideSignalInstance *item = PyObject_New(PySideSignalInstance, PySideSignalInstance_TypeF()); if (!root) @@ -1159,12 +1182,6 @@ static bool compareSignals(const PySideSignalData::Signature &sig1, return sig1.signature.isEmpty() && !sig2.signature.isEmpty(); } -static PyObject *buildQtCompatible(const QByteArray &signature) -{ - const auto ba = QT_SIGNAL_SENTINEL + signature; - return Shiboken::String::fromStringAndSize(ba, ba.size()); -} - void registerSignals(PyTypeObject *pyObj, const QMetaObject *metaObject) { using Signature = PySideSignalData::Signature; @@ -1213,7 +1230,9 @@ void registerSignals(PyTypeObject *pyObj, const QMetaObject *metaObject) PyObject *getObject(PySideSignalInstance *signal) { - return signal->d->shared->source; + if (auto *qSender = sender(signal)) + return getWrapperForQObject(qSender, signal->d->shared->sourceType); + return nullptr; } const char *getSignature(PySideSignalInstance *signal) diff --git a/sources/pyside6/libpyside/pysidesignal.h b/sources/pyside6/libpyside/pysidesignal.h index d1f367470..f9a8e0565 100644 --- a/sources/pyside6/libpyside/pysidesignal.h +++ b/sources/pyside6/libpyside/pysidesignal.h @@ -63,7 +63,8 @@ PYSIDE_API void registerSignals(PyTypeObject *pyObj, const QMetaObject *metaObje * @param methods a list of QMetaMethod wich contains the supported signature * @return Return a new reference to PyObject* of type PySideSignal **/ -PYSIDE_API PySideSignalInstance *newObjectFromMethod(PyObject *source, const QList<QMetaMethod> &methods); +PYSIDE_API PySideSignalInstance *newObjectFromMethod(QObject *sourceQObject, PyObject *source, + const QList<QMetaMethod> &methods); /** * This function initializes the Signal object by creating a PySideSignalInstance diff --git a/sources/pyside6/libpyside/pysidesignal_p.h b/sources/pyside6/libpyside/pysidesignal_p.h index 259da0f34..2c4415b0e 100644 --- a/sources/pyside6/libpyside/pysidesignal_p.h +++ b/sources/pyside6/libpyside/pysidesignal_p.h @@ -8,6 +8,8 @@ #include <QtCore/QByteArray> #include <QtCore/QList> +#include <QtCore/QObject> +#include <QtCore/QPointer> #include <memory> @@ -40,8 +42,8 @@ extern "C" struct PySideSignalInstanceShared { - PyObject *source = nullptr; - bool deleted = false; + QPointer<QObject> source; + PyTypeObject *sourceType = nullptr; }; using PySideSignalInstanceSharedPtr = std::shared_ptr<PySideSignalInstanceShared>; diff --git a/sources/pyside6/tests/QtCore/destroysignal_test.py b/sources/pyside6/tests/QtCore/destroysignal_test.py index 0384821f4..ae7943ce9 100644 --- a/sources/pyside6/tests/QtCore/destroysignal_test.py +++ b/sources/pyside6/tests/QtCore/destroysignal_test.py @@ -70,7 +70,7 @@ class TestDestroyNoConnect(unittest.TestCase): def testSignalDestroyedinConnect(self): # PYSIDE-2328: Connect to signal of temporary - with self.assertRaises(RuntimeError): + with self.assertRaises(TypeError): Foo().s.connect(None) |