diff --git a/iioutil/include/iioutil/connection.h b/iioutil/include/iioutil/connection.h new file mode 100644 index 000000000..c6d868816 --- /dev/null +++ b/iioutil/include/iioutil/connection.h @@ -0,0 +1,65 @@ +#ifndef CONNECTION_H +#define CONNECTION_H +#include "scopy-iioutil_export.h" +#include "commandqueue.h" +#include +#include + +namespace scopy { +class SCOPY_IIOUTIL_EXPORT Connection : public QObject +{ + Q_OBJECT +public: + Connection(QString uri); + + const QString &uri() const; + CommandQueue *commandQueue() const; + struct iio_context *context() const; + int refCount() const; + +protected: + ~Connection(); + + /** + * @brief open + * Initialize the connection if not previously opened. + * If previously opened, increase the internal refCount. + */ + void open(); + + /** + * @brief close + * Decrement the internal refCount. + * Emit the aboutToBeDestroyed() signal if refCount is zero. + */ + void close(); + + /** + * @brief closeAll + * Reset the internal refCount to zero. + * Force close the Connection. + * Emit the aboutToBeDestroyed() signal. + */ + void closeAll(); + +Q_SIGNALS: + /** + * @brief aboutToBeDestroyed + * Connection clients should handle deinitialization + * of their iio_context/CommandQueue related operations + * in a slot connected to this signal. + * After the signal is emitted, the Connection object + * will no longer be valid. + */ + void aboutToBeDestroyed(); + +private: + friend class ConnectionProvider; + QString m_uri; + CommandQueue *m_commandQueue; + struct iio_context *m_context; + int m_refCount = 0; +}; +} // namespace scopy + +#endif // CONNECTION_H diff --git a/iioutil/include/iioutil/connectionprovider.h b/iioutil/include/iioutil/connectionprovider.h new file mode 100644 index 000000000..024002d3f --- /dev/null +++ b/iioutil/include/iioutil/connectionprovider.h @@ -0,0 +1,39 @@ +#ifndef CONNECTIONPROVIDER_H +#define CONNECTIONPROVIDER_H + +#include "scopy-iioutil_export.h" +#include "connection.h" + +#include +#include +#include + +namespace scopy { +class SCOPY_IIOUTIL_EXPORT ConnectionProvider : public QObject +{ + Q_OBJECT +protected: + ConnectionProvider(QObject *parent = nullptr); + ~ConnectionProvider(); + +public: + ConnectionProvider(ConnectionProvider &other) = delete; + void operator=(const ConnectionProvider &) = delete; + + static ConnectionProvider *GetInstance(); + static Connection *open(QString uri); + static void close(QString uri); + static void closeAll(QString uri); + +private: + Connection *_open(QString uri); + void _close(QString uri); + void _closeAll(QString uri); + void _closeAndRemove(QString uri); + static ConnectionProvider *pinstance_; + static std::mutex mutex_; + QMap map; +}; +} // namespace scopy + +#endif // CONNECTIONPROVIDER_H diff --git a/iioutil/src/connection.cpp b/iioutil/src/connection.cpp new file mode 100644 index 000000000..7c1275763 --- /dev/null +++ b/iioutil/src/connection.cpp @@ -0,0 +1,64 @@ +#include "connection.h" + +using namespace scopy; + +Connection::Connection(QString uri) +{ + this->m_uri = uri; + this->m_context = nullptr; + this->m_commandQueue = nullptr; + this->m_refCount = 0; +} + +Connection::~Connection() +{ + if(this->m_commandQueue) { + delete this->m_commandQueue; + this->m_commandQueue = nullptr; + } + if(this->m_context) { + iio_context_destroy(this->m_context); + this->m_context = nullptr; + } +} + +const QString &Connection::uri() const { return m_uri; } + +CommandQueue *Connection::commandQueue() const { return m_commandQueue; } + +iio_context *Connection::context() const { return m_context; } + +int Connection::refCount() const { return m_refCount; } + +void Connection::open() +{ + if(!this->m_context) { + this->m_context = iio_create_context_from_uri(this->m_uri.toStdString().c_str()); + if(this->m_context) { + this->m_commandQueue = new CommandQueue(); + this->m_refCount++; + } + } else { + this->m_refCount++; + } +} + +void Connection::closeAll() +{ + this->m_refCount = 0; + close(); +} + +void Connection::close() +{ + this->m_refCount--; + if(this->m_refCount <= 0) { + /* If the open() and close() number of calls done by a client + * is mismatched, all the remaining clients should be notified of the + * destruction. */ + this->m_refCount = 0; + Q_EMIT aboutToBeDestroyed(); + } +} + +#include "moc_connection.cpp" diff --git a/iioutil/src/connectionprovider.cpp b/iioutil/src/connectionprovider.cpp new file mode 100644 index 000000000..416e588c3 --- /dev/null +++ b/iioutil/src/connectionprovider.cpp @@ -0,0 +1,93 @@ +#include "connectionprovider.h" +#include +#include + +Q_LOGGING_CATEGORY(CAT_CONNECTIONMGR, "ConnectionProvider") + +using namespace scopy; + +ConnectionProvider *ConnectionProvider::pinstance_{nullptr}; +std::mutex ConnectionProvider::mutex_; + +scopy::ConnectionProvider::ConnectionProvider(QObject *parent) + : QObject(parent) +{ + qDebug(CAT_CONNECTIONMGR) << "ConnectionProvider object ctor"; +} + +scopy::ConnectionProvider::~ConnectionProvider() { qDebug(CAT_CONNECTIONMGR) << "ConnectionProvider object dtor"; } + +ConnectionProvider *ConnectionProvider::GetInstance() +{ + std::lock_guard lock(mutex_); + if(pinstance_ == nullptr) { + pinstance_ = new ConnectionProvider(QApplication::instance()); // singleton has the app as parent + } else { + qDebug(CAT_CONNECTIONMGR) << "ConnectionProvider: Got instance from singleton"; + } + return pinstance_; +} + +Connection *ConnectionProvider::open(QString uri) { return ConnectionProvider::GetInstance()->_open(uri); } + +Connection *ConnectionProvider::_open(QString uri) +{ + std::lock_guard lock(mutex_); + Connection *connectionObject = nullptr; + if(!map.contains(uri)) { + connectionObject = new Connection(uri); + map.insert(uri, connectionObject); + qDebug(CAT_CONNECTIONMGR) << uri << " created in map "; + } else { + connectionObject = map.value(uri); + qDebug(CAT_CONNECTIONMGR) << uri << " found in map "; + } + connectionObject->open(); + if(connectionObject->refCount() == 0) { + qDebug(CAT_CONNECTIONMGR) << uri << " Connection: failed to open, removed from map."; + map.remove(uri); + delete connectionObject; + return nullptr; + } + qDebug(CAT_CONNECTIONMGR) << uri << " Connection: open, refcount++ = " << connectionObject->refCount(); + return connectionObject; +} + +void ConnectionProvider::closeAll(QString uri) { return ConnectionProvider::GetInstance()->_closeAll(uri); } + +void ConnectionProvider::close(QString uri) { return ConnectionProvider::GetInstance()->_close(uri); } + +void ConnectionProvider::_closeAll(QString uri) +{ + std::lock_guard lock(mutex_); + if(map.contains(uri)) { + map.value(uri)->closeAll(); + } + _closeAndRemove(uri); +} + +void ConnectionProvider::_close(QString uri) +{ + std::lock_guard lock(mutex_); + if(map.contains(uri)) { + map.value(uri)->close(); + } + _closeAndRemove(uri); +} + +void ConnectionProvider::_closeAndRemove(QString uri) +{ + if(map.contains(uri)) { + qDebug(CAT_CONNECTIONMGR) + << uri << " Connection: closing - found in map - refcnt-- = " << map.value(uri)->refCount(); + if(map.value(uri)->refCount() == 0) { + delete map[uri]; + map.remove(uri); + qDebug(CAT_CONNECTIONMGR) << uri << " Connection: destroyed, removed from map"; + } + } else { + qInfo(CAT_CONNECTIONMGR) << uri << " Connection: not found in map. nop"; + } +} + +#include "moc_connectionprovider.cpp" diff --git a/iioutil/test/CMakeLists.txt b/iioutil/test/CMakeLists.txt index dada5cfc2..f38f34e5b 100644 --- a/iioutil/test/CMakeLists.txt +++ b/iioutil/test/CMakeLists.txt @@ -3,3 +3,4 @@ cmake_minimum_required(VERSION 3.5) include(ScopyTest) setup_scopy_tests(iiocommandqueue) +setup_scopy_tests(connectionprovider) diff --git a/iioutil/test/tst_connectionprovider.cpp b/iioutil/test/tst_connectionprovider.cpp new file mode 100644 index 000000000..7a7870c04 --- /dev/null +++ b/iioutil/test/tst_connectionprovider.cpp @@ -0,0 +1,209 @@ +#include +#include + +#include + +using namespace scopy; + +class TestCommandAdd : public Command +{ + Q_OBJECT +public: + explicit TestCommandAdd(int a, int b, QObject *parent) + : m_a(a) + , m_b(b) + { + this->setParent(parent); + m_cmdResult = new CommandResult(); + } + + virtual ~TestCommandAdd() { qDebug() << "TestCommand deleted"; } + + virtual void execute() override + { + Q_EMIT started(this); + m_cmdResult->errorCode = m_a + m_b; + Q_EMIT finished(this); + } + +private: + int m_a, m_b; +}; + +class Plugin1 : public QObject +{ + Q_OBJECT +public: + explicit Plugin1(QString boarduri) + { + uri = boarduri; + conn = ConnectionProvider::open(uri); + if(conn) { + connect(conn, &Connection::aboutToBeDestroyed, this, &Plugin1::handleClose); + commandQueue = conn->commandQueue(); + ctx = conn->context(); + } else { + conn = nullptr; + commandQueue = nullptr; + uri = nullptr; + } + } + + virtual ~Plugin1() { ConnectionProvider::close(uri); } + + void close() + { + qInfo() << "Plugin1 close connection"; + ConnectionProvider::close(uri); + } + + void closeAll() { ConnectionProvider::closeAll(uri); } + + const QString &getUri() const { return uri; } + + Connection *getConn() const { return conn; } + + CommandQueue *getCommandQueue() const { return commandQueue; } + + iio_context *getCtx() const { return ctx; } + +public Q_SLOTS: + void handleClose() + { + // notify any thread that might be using command queue or ctx + qInfo() << "Plugin1 handle close on aboutToBeDestroyed() signal"; + conn = nullptr; + commandQueue = nullptr; + uri = nullptr; + } + +private: + QString uri; + Connection *conn; + CommandQueue *commandQueue; + struct iio_context *ctx; +}; + +class TST_ConnectionProvider : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testReferenceCount(); + void testMismatchedOpenClose(); + void testSignalAboutToBeDestroyed(); + void testForceClose(); + void testConnectionCmdQ(); + +private: + int TEST_A = 100; + int TEST_B = 20; +}; + +void TST_ConnectionProvider::testReferenceCount() +{ + QString uri = "ip:192.168.2.1"; + Plugin1 *p1 = new Plugin1(uri); + QVERIFY2(p1->getConn()->refCount() == 1, "P1 object connection not opened"); + + Plugin1 *p2 = new Plugin1(uri); + QVERIFY2(p1->getConn()->refCount() == 2, "P2 object connection not opened"); + + Plugin1 *p3 = new Plugin1(uri); + QVERIFY2(p1->getConn()->refCount() == 3, "P3 object connection not opened"); + + p1->close(); + QVERIFY2(p1->getConn()->refCount() == 2, "P1 object connection not closed"); + p2->close(); + QVERIFY2(p2->getConn()->refCount() == 1, "P2 object connection not closed"); + p3->close(); + QVERIFY2(p3->getConn() == nullptr, "P3 object connection not closed"); +} + +void TST_ConnectionProvider::testMismatchedOpenClose() +{ + QString uri = "ip:192.168.2.1"; + Plugin1 *p1 = new Plugin1(uri); + Plugin1 *p2 = new Plugin1(uri); + + QVERIFY2(p1->getConn()->refCount() == 2, "P1 object connection not opened"); + + p1->close(); + p1->close(); + QVERIFY2(p1->getConn() == nullptr, "P1 object connection not closed"); + p2->close(); +} + +void TST_ConnectionProvider::testSignalAboutToBeDestroyed() +{ + QString uri = "ip:192.168.2.1"; + Plugin1 *p1 = new Plugin1(uri); + Plugin1 *p2 = new Plugin1(uri); + Plugin1 *p3 = new Plugin1(uri); + + QVERIFY2(p1->getConn() != nullptr, "P1 object connection not opened"); + QVERIFY2(p2->getConn() != nullptr, "P2 object connection not opened"); + QVERIFY2(p3->getConn() != nullptr, "P3 object connection not opened"); + + p1->close(); + p2->close(); + p3->close(); + QVERIFY2(p1->getConn() == nullptr, "P1 object connection not closed"); + QVERIFY2(p2->getConn() == nullptr, "P2 object connection not closed"); + QVERIFY2(p3->getConn() == nullptr, "P3 object connection not closed"); +} + +void TST_ConnectionProvider::testForceClose() +{ + QString uri = "ip:192.168.2.1"; + Plugin1 *p1 = new Plugin1(uri); + Plugin1 *p2 = new Plugin1(uri); + Plugin1 *p3 = new Plugin1(uri); + + QVERIFY2(p1->getConn() != nullptr, "P1 object connection not opened"); + QVERIFY2(p2->getConn() != nullptr, "P2 object connection not opened"); + QVERIFY2(p3->getConn() != nullptr, "P3 object connection not opened"); + + p3->closeAll(); + QVERIFY2(p1->getConn() == nullptr, "P1 object connection not closed"); + QVERIFY2(p2->getConn() == nullptr, "P2 object connection not closed"); + QVERIFY2(p3->getConn() == nullptr, "P3 object connection not closed"); +} + +void TST_ConnectionProvider::testConnectionCmdQ() +{ + Command *cmd1 = new TestCommandAdd(TEST_A, TEST_B, nullptr); + QString uri = "ip:192.168.2.1"; + Plugin1 *p1 = new Plugin1(uri); + Plugin1 *p2 = new Plugin1(uri); + Plugin1 *p3 = new Plugin1(uri); + + QVERIFY2(p1->getConn() != nullptr, "P1 object connection not opened"); + QVERIFY2(p2->getConn() != nullptr, "P2 object connection not opened"); + QVERIFY2(p3->getConn() != nullptr, "P3 object connection not opened"); + + connect( + cmd1, &scopy::Command::finished, this, + [=](scopy::Command *cmd) { + if(!cmd) { + cmd = dynamic_cast(QObject::sender()); + } + TestCommandAdd *tcmd = dynamic_cast(cmd); + QVERIFY2(tcmd != nullptr, "Capture command is null"); + QVERIFY2(tcmd == cmd1, "Captured command not the expected one"); + QVERIFY2(tcmd->getReturnCode() == (TEST_B + TEST_A), "TestCommandAdd did not execute"); + }, + Qt::QueuedConnection); + + p1->getCommandQueue()->enqueue(cmd1); + + QTest::qWait(100); + p3->close(); + p2->close(); + p1->close(); + QVERIFY2(p1->getConn() == nullptr, "P1 object connection not closed"); + QVERIFY2(p2->getConn() == nullptr, "P2 object connection not closed"); + QVERIFY2(p3->getConn() == nullptr, "P3 object connection not closed"); +} + +QTEST_MAIN(TST_ConnectionProvider) +#include "tst_connectionprovider.moc"