From d32854ff4fc6a0fa4a0ae3bae7a35aa3cf28da83 Mon Sep 17 00:00:00 2001 From: 100029962 Date: Mon, 17 Apr 2023 10:56:38 +0200 Subject: [PATCH] [dcmnet] Implement TCP connect cancellation support in DcmSCU --- dcmnet/include/dcmtk/dcmnet/scu.h | 8 ++- dcmnet/libsrc/scu.cc | 15 ++++ dcmnet/tests/tests.cc | 3 + dcmnet/tests/tscuscp.cc | 112 ++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) diff --git a/dcmnet/include/dcmtk/dcmnet/scu.h b/dcmnet/include/dcmtk/dcmnet/scu.h index 9620c3c925..6a0c55727a 100644 --- a/dcmnet/include/dcmtk/dcmnet/scu.h +++ b/dcmnet/include/dcmtk/dcmnet/scu.h @@ -230,8 +230,14 @@ class DCMTK_DCMNET_EXPORT DcmSCU /** Negotiate association by using presentation contexts and parameters as defined by * earlier function calls. If negotiation fails, there is no need to close the association * or to do anything else with this class. + * @param tcpCancelToken [in] Optional cancellation token which is checked periodically + * for cancellation. If the token is cancelled during TCP + * connect phase, the TCP connect attempts are stopped and this + * function returns failure. * @return EC_Normal if negotiation was successful, otherwise error code. - * NET_EC_AlreadyConnected if SCU is already connected. + * NET_EC_AlreadyConnected if SCU is already connected. DULC_TCPINITERROR is returned + * if the TCP connect attempt was cancelled before the connection was established + * */ virtual OFCondition negotiateAssociation(IDcmCancelToken* tcpCancelToken = NULL); diff --git a/dcmnet/libsrc/scu.cc b/dcmnet/libsrc/scu.cc index 0d66d50cce..9a515273de 100644 --- a/dcmnet/libsrc/scu.cc +++ b/dcmnet/libsrc/scu.cc @@ -32,6 +32,15 @@ #include /* for zlibVersion() */ #endif +static bool CancelTcpConnect(void* context) +{ + if (!context) + return false; + + const IDcmCancelToken* token = OFstatic_cast(const IDcmCancelToken*, context); + return token->IsCanceled(); +} + DcmSCU::DcmSCU() : m_assoc(NULL) , m_net(NULL) @@ -129,6 +138,9 @@ OFCondition DcmSCU::initNetwork() return cond; } + m_params->DULparams.tcpPollInterval = m_tcpPollInterval; + m_params->DULparams.tcpConnectCanceled = CancelTcpConnect; + /* sets this application's title and the called application's title in the params */ /* structure. The default values are "ANY-SCU" and "ANY-SCP". */ ASC_setAPTitles(m_params, m_ourAETitle.c_str(), m_peerAETitle.c_str(), NULL); @@ -272,6 +284,9 @@ OFCondition DcmSCU::negotiateAssociation(IDcmCancelToken* tcpCancelToken) else DCMNET_DEBUG("Request Parameters:" << OFendl << ASC_dumpParameters(tempStr, m_params, ASC_ASSOC_RQ)); + + m_params->DULparams.tcpCancelContext = tcpCancelToken; + /* create association, i.e. try to establish a network connection to another */ /* DICOM application. This call creates an instance of T_ASC_Association*. */ DCMNET_INFO("Requesting Association"); diff --git a/dcmnet/tests/tests.cc b/dcmnet/tests/tests.cc index e7a2b438ba..aa04e6ec17 100644 --- a/dcmnet/tests/tests.cc +++ b/dcmnet/tests/tests.cc @@ -51,6 +51,9 @@ OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_fails_when_requestedsopinstance_is_em OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_succeeds_and_modifies_instance_when_scp_has_instance); OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_succeeds_and_sets_responsestatuscode_from_scp_when_scp_sets_error_status); +OFTEST_REGISTER(dcmnet_scu_negotiateAssociation_fails_when_token_is_initially_cancelled); +OFTEST_REGISTER(dcmnet_scu_negotiateAssociation_fails_when_token_gets_cancelled_after_3_calls); +OFTEST_REGISTER(dcmnet_scu_negotiateAssociation_fails_when_connectionTimeout_elapses); #endif // WITH_THREADS OFTEST_MAIN("dcmnet") diff --git a/dcmnet/tests/tscuscp.cc b/dcmnet/tests/tscuscp.cc index 0e1d986a38..c5525dae19 100644 --- a/dcmnet/tests/tscuscp.cc +++ b/dcmnet/tests/tscuscp.cc @@ -505,6 +505,118 @@ OFTEST(dcmnet_scu_getConectionTimeout_returns_scu_tcp_connection_timeout) OFCHECK(scu.getConnectionTimeout() == 42); } +struct CancelToken : IDcmCancelToken +{ + CancelToken(Uint32 cancelAfterNumCalls) + : m_canceled(false) + , m_cancelAfterNumCalls(cancelAfterNumCalls) + , m_callCount(0) + {} + + bool IsCanceled() const /* override */ + { + ++m_callCount; + if (m_callCount > m_cancelAfterNumCalls) + m_canceled = true; + + return m_canceled; + } + + mutable bool m_canceled; + const Uint32 m_cancelAfterNumCalls; + mutable Uint32 m_callCount; +}; + + +struct TcpCancelFixture +{ + TcpCancelFixture() + { + m_scu.setPeerAETitle("ACCEPTOR"); + m_scu.setAETitle("REQUESTOR"); + m_scu.setPeerHostName("localhost"); + m_scu.setPeerPort(GetUnusedPort()); + + OFList ts; + ts.push_back(UID_LittleEndianImplicitTransferSyntax); + OFCHECK(m_scu.addPresentationContext(UID_VerificationSOPClass, ts, ASC_SC_ROLE_DEFAULT).good()); + } + + // Temporarily start an SCP to get an unused port + // Note: The port is not reserved after this call returns + static Uint16 GetUnusedPort() + { + TestSCP m_scp; + DcmSCPConfig& config = m_scp.getConfig(); + configure_scp_for_echo(config, 0, ASC_SC_ROLE_SCP); + config.setAETitle("ACCEPTOR"); + config.setConnectionBlockingMode(DUL_NOBLOCK); + config.setConnectionTimeout(4); + OFCHECK(m_scp.openListenPort().good()); + const Uint16 port = config.getPort(); + m_scp.join(); + return port; + } + + DcmSCU m_scu; + +}; + +OFTEST(dcmnet_scu_negotiateAssociation_fails_when_token_is_initially_cancelled) +{ + TcpCancelFixture m_fixture; + DcmSCU& scu = m_fixture.m_scu; + + scu.setTcpPollInterval(1); + scu.setConnectionTimeout(60); + + OFCHECK(scu.initNetwork().good()); + + CancelToken tok(0); + tok.m_canceled = true; + + const OFCondition result = scu.negotiateAssociation(&tok); + + OFCHECK(result.code() == DULC_TCPINITERROR); + OFCHECK(tok.m_callCount == 1); +} + +OFTEST(dcmnet_scu_negotiateAssociation_fails_when_token_gets_cancelled_after_3_calls) +{ + TcpCancelFixture m_fixture; + DcmSCU& scu = m_fixture.m_scu; + + scu.setTcpPollInterval(1); + scu.setConnectionTimeout(60); + + OFCHECK(scu.initNetwork().good()); + + CancelToken tok(3); + + const OFCondition result = scu.negotiateAssociation(&tok); + + OFCHECK(result.code() == DULC_TCPINITERROR); + OFCHECK(tok.m_callCount == 4); +} + +OFTEST(dcmnet_scu_negotiateAssociation_fails_when_connectionTimeout_elapses) +{ + + TcpCancelFixture m_fixture; + DcmSCU& scu = m_fixture.m_scu; + + scu.setTcpPollInterval(1); + scu.setConnectionTimeout(5); + + CancelToken tok(15); // Should never be called 15 times since connection timeout is reached first + + OFCHECK(scu.initNetwork().good()); + const OFCondition result = scu.negotiateAssociation(&tok); + + OFCHECK(result.code() == DULC_TCPINITERROR); + OFCHECK(!tok.m_canceled); +} + /** Helper function for testing role selection, test "dcmnet_scp_role_selection". * @param r_req The role selection setting from the association requestor