# HG changeset patch # User Sebastien Jodogne # Date 1589992651 -7200 # Node ID 4d36d6e64c6d36addaea1cce2d2e58e74e594fef # Parent 6e14f2da7c7e338f67c2ec5a27cad7cf7738c2fc# Parent 76a24be1291270b4839b7839fcf0484e3a17220f integration c-get->mainline diff -r 6e14f2da7c7e -r 4d36d6e64c6d CMakeLists.txt --- a/CMakeLists.txt Wed May 20 16:42:44 2020 +0200 +++ b/CMakeLists.txt Wed May 20 18:37:31 2020 +0200 @@ -73,6 +73,7 @@ OrthancServer/OrthancHttpHandler.cpp OrthancServer/OrthancInitialization.cpp OrthancServer/OrthancMoveRequestHandler.cpp + OrthancServer/OrthancGetRequestHandler.cpp OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/OrthancRestApi/OrthancRestApi.cpp OrthancServer/OrthancRestApi/OrthancRestArchive.cpp diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/DicomServer.cpp --- a/Core/DicomNetworking/DicomServer.cpp Wed May 20 16:42:44 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.cpp Wed May 20 18:37:31 2020 +0200 @@ -92,6 +92,7 @@ modalities_ = NULL; findRequestHandlerFactory_ = NULL; moveRequestHandlerFactory_ = NULL; + getRequestHandlerFactory_ = NULL; storeRequestHandlerFactory_ = NULL; worklistRequestHandlerFactory_ = NULL; storageCommitmentFactory_ = NULL; @@ -244,6 +245,29 @@ } } + void DicomServer::SetGetRequestHandlerFactory(IGetRequestHandlerFactory& factory) + { + Stop(); + getRequestHandlerFactory_ = &factory; + } + + bool DicomServer::HasGetRequestHandlerFactory() const + { + return (getRequestHandlerFactory_ != NULL); + } + + IGetRequestHandlerFactory& DicomServer::GetGetRequestHandlerFactory() const + { + if (HasGetRequestHandlerFactory()) + { + return *getRequestHandlerFactory_; + } + else + { + throw OrthancException(ErrorCode_NoCGetHandler); + } + } + void DicomServer::SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& factory) { Stop(); diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/DicomServer.h --- a/Core/DicomNetworking/DicomServer.h Wed May 20 16:42:44 2020 +0200 +++ b/Core/DicomNetworking/DicomServer.h Wed May 20 18:37:31 2020 +0200 @@ -39,6 +39,7 @@ #include "IFindRequestHandlerFactory.h" #include "IMoveRequestHandlerFactory.h" +#include "IGetRequestHandlerFactory.h" #include "IStoreRequestHandlerFactory.h" #include "IWorklistRequestHandlerFactory.h" #include "IStorageCommitmentRequestHandlerFactory.h" @@ -81,6 +82,7 @@ IRemoteModalities* modalities_; IFindRequestHandlerFactory* findRequestHandlerFactory_; IMoveRequestHandlerFactory* moveRequestHandlerFactory_; + IGetRequestHandlerFactory* getRequestHandlerFactory_; IStoreRequestHandlerFactory* storeRequestHandlerFactory_; IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_; IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_; @@ -116,6 +118,10 @@ bool HasMoveRequestHandlerFactory() const; IMoveRequestHandlerFactory& GetMoveRequestHandlerFactory() const; + void SetGetRequestHandlerFactory(IGetRequestHandlerFactory& handler); + bool HasGetRequestHandlerFactory() const; + IGetRequestHandlerFactory& GetGetRequestHandlerFactory() const; + void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler); bool HasStoreRequestHandlerFactory() const; IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const; diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/IGetRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IGetRequestHandler.h Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,80 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include + +#include "../../Core/DicomFormat/DicomMap.h" + +#include +#include + + +namespace Orthanc +{ + class IGetRequestHandler : boost::noncopyable + { + public: + enum Status + { + Status_Success, + Status_Failure, + Status_Warning + }; + + virtual ~IGetRequestHandler() + { + } + + virtual bool Handle(const DicomMap& input, + const std::string& originatorIp, + const std::string& originatorAet, + const std::string& calledAet, + uint32_t timeout) = 0; + + virtual unsigned int GetSubOperationCount() const = 0; + + virtual Status DoNext(T_ASC_Association *) = 0; + + virtual unsigned int GetRemainingCount() const = 0; + + virtual unsigned int GetCompletedCount() const = 0; + + virtual unsigned int GetWarningCount() const = 0; + + virtual unsigned int GetFailedCount() const = 0; + + virtual const std::string& GetFailedUids() const = 0; + }; +} diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/IGetRequestHandlerFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/IGetRequestHandlerFactory.h Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,49 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "IGetRequestHandler.h" + +namespace Orthanc +{ + class IGetRequestHandlerFactory : public boost::noncopyable + { + public: + virtual ~IGetRequestHandlerFactory() + { + } + + virtual IGetRequestHandler* ConstructGetRequestHandler() = 0; + }; +} diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/Internals/CommandDispatcher.cpp --- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed May 20 16:42:44 2020 +0200 +++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp Wed May 20 18:37:31 2020 +0200 @@ -90,6 +90,7 @@ #include "FindScp.h" #include "StoreScp.h" #include "MoveScp.h" +#include "GetScp.h" #include "../../Compatibility.h" #include "../../Toolbox.h" #include "../../Logging.h" @@ -275,6 +276,8 @@ OFString sprofile; OFString temp_str; + + cond = ASC_receiveAssociation(net, &assoc, /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, NULL, NULL, @@ -371,6 +374,14 @@ knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel); knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel); } + + // For C-GET + if (server.HasGetRequestHandlerFactory()) + { + knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel); + knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel); + } + cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, @@ -511,6 +522,10 @@ assert(static_cast(count) == numberOfDcmAllStorageSOPClassUIDs); #endif + // now that C-GET SCP is always enabled, the first branch of this if is useless + // TO BE ANALYZED by SJ + if (!server.HasGetRequestHandlerFactory()) // dcmqrsrv.cc line 828 + { cond = ASC_acceptContextsWithPreferredTransferSyntaxes( assoc->params, dcmAllStorageSOPClassUIDs, count, @@ -521,6 +536,55 @@ AssociationCleanup(assoc); return NULL; } + } + else // see dcmqrsrv.cc lines 839 - 876 + { + /* accept storage syntaxes with proposed role */ + T_ASC_PresentationContext pc; + T_ASC_SC_ROLE role; + int npc = ASC_countPresentationContexts(assoc->params); + for (int i = 0; i < npc; i++) + { + ASC_getPresentationContext(assoc->params, i, &pc); + if (dcmIsaStorageSOPClassUID(pc.abstractSyntax)) + { + /* + ** We are prepared to accept whatever role the caller proposes. + ** Normally we can be the SCP of the Storage Service Class. + ** When processing the C-GET operation we can be the SCU of the Storage Service Class. + */ + role = pc.proposedRole; + + /* + ** Accept in the order "least wanted" to "most wanted" transfer + ** syntax. Accepting a transfer syntax will override previously + ** accepted transfer syntaxes. + */ + for (int k = (int) storageTransferSyntaxes.size() - 1; k >= 0; k--) + { + for (int j = 0; j < (int)pc.transferSyntaxCount; j++) + { + /* if the transfer syntax was proposed then we can accept it + * appears in our supported list of transfer syntaxes + */ + if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxes[k]) == 0) + { + cond = ASC_acceptPresentationContext( + assoc->params, pc.presentationContextID, storageTransferSyntaxes[k], role); + if (cond.bad()) + { + LOG(INFO) << cond.text(); + AssociationCleanup(assoc); + return NULL; + } + } + } + } + } + } /* for */ + + } + if (!server.HasApplicationEntityFilter() || server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet)) @@ -733,6 +797,11 @@ request = DicomRequestType_Move; supported = true; break; + + case DIMSE_C_GET_RQ: + request = DicomRequestType_Get; + supported = true; + break; case DIMSE_C_FIND_RQ: request = DicomRequestType_Find; @@ -807,6 +876,19 @@ } } break; + + case DicomRequestType_Get: + if (server_.HasGetRequestHandlerFactory()) // Should always be true + { + std::auto_ptr handler + (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler()); + + if (handler.get() != NULL) + { + cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_); + } + } + break; case DicomRequestType_Find: if (server_.HasFindRequestHandlerFactory() || // Should always be true diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/Internals/GetScp.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/GetScp.cpp Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,289 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + + + +/*========================================================================= + + This file is based on portions of the following project: + + Program: DCMTK 3.6.0 + Module: http://dicom.offis.de/dcmtk.php.en + + Copyright (C) 1994-2011, OFFIS e.V. + All rights reserved. + + This software and supporting documentation were developed by + + OFFIS e.V. + R&D Division Health + Escherweg 2 + 26121 Oldenburg, Germany + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + - Neither the name of OFFIS nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + =========================================================================*/ + + +#include "../../PrecompiledHeaders.h" +#include +#include +#include "GetScp.h" + +#include + +#include "../../DicomParsing/FromDcmtkBridge.h" +#include "../../DicomParsing/ToDcmtkBridge.h" +#include "../../Logging.h" +#include "../../OrthancException.h" + +#include + + +namespace Orthanc +{ + namespace + { + struct GetScpData + { + // Handle returns void. + IGetRequestHandler* handler_; + DcmDataset* lastRequest_; + T_ASC_Association * assoc_; + + std::string remoteIp_; + std::string remoteAet_; + std::string calledAet_; + int timeout_; + + GetScpData() + { + handler_ = NULL; + lastRequest_ = NULL; + assoc_ = NULL; + }; + }; + + static DcmDataset *BuildFailedInstanceList(const std::string& failedUIDs) + { + if (failedUIDs.empty()) + { + return NULL; + } + else + { + std::unique_ptr rspIds(new DcmDataset()); + + if (!DU_putStringDOElement(rspIds.get(), DCM_FailedSOPInstanceUIDList, failedUIDs.c_str())) + { + throw OrthancException(ErrorCode_InternalError, + "getSCP: failed to build DCM_FailedSOPInstanceUIDList"); + } + + return rspIds.release(); + } + } + + static void GetScpCallback( + /* in */ + void *callbackData, + OFBool cancelled, + T_DIMSE_C_GetRQ *request, + DcmDataset *requestIdentifiers, + int responseCount, + /* out */ + T_DIMSE_C_GetRSP *response, + DcmDataset **responseIdentifiers, + DcmDataset **statusDetail) + { + bzero(response, sizeof(T_DIMSE_C_GetRSP)); + *statusDetail = NULL; + *responseIdentifiers = NULL; + + GetScpData& data = *reinterpret_cast(callbackData); + if (data.lastRequest_ == NULL) + { + DicomMap input; + FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers); + + try + { + if (!data.handler_->Handle( + input, data.remoteIp_, data.remoteAet_, data.calledAet_, + data.timeout_ < 0 ? 0 : static_cast(data.timeout_))) + { + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + } + catch (OrthancException& e) + { + // Internal error! + LOG(ERROR) << "IGetRequestHandler Failed: " << e.What(); + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + + data.lastRequest_ = requestIdentifiers; + } + else if (data.lastRequest_ != requestIdentifiers) + { + // Internal error! + LOG(ERROR) << "IGetRequestHandler Failed: Internal error lastRequestIdentifier"; + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + + if (data.handler_->GetRemainingCount() == 0) + { + response->DimseStatus = STATUS_Success; + } + else + { + IGetRequestHandler::Status status; + + try + { + status = data.handler_->DoNext(data.assoc_); + } + catch (OrthancException& e) + { + // Internal error! + LOG(ERROR) << "IGetRequestHandler Failed: " << e.What(); + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + return; + } + + if (status == STATUS_Success) + { + if (responseCount < static_cast(data.handler_->GetRemainingCount())) + { + response->DimseStatus = STATUS_Pending; + } + else + { + response->DimseStatus = STATUS_Success; + } + } + else + { + response->DimseStatus = STATUS_GET_Failed_UnableToProcess; + + if (data.handler_->GetFailedCount() > 0 || + data.handler_->GetWarningCount() > 0) + { + response->DimseStatus = STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures; + } + + /* + * if all the sub-operations failed then we need to generate + * a failed or refused status. cf. DICOM part 4, C.4.3.3.1 + * we choose to generate a "Refused - Out of Resources - + * Unable to perform suboperations" status. + */ + if ((data.handler_->GetFailedCount() > 0) && + ((data.handler_->GetCompletedCount() + + data.handler_->GetWarningCount()) == 0)) + { + response->DimseStatus = STATUS_GET_Refused_OutOfResourcesSubOperations; + } + + *responseIdentifiers = BuildFailedInstanceList(data.handler_->GetFailedUids()); + } + } + + response->NumberOfRemainingSubOperations = data.handler_->GetRemainingCount(); + response->NumberOfCompletedSubOperations = data.handler_->GetCompletedCount(); + response->NumberOfFailedSubOperations = data.handler_->GetFailedCount(); + response->NumberOfWarningSubOperations = data.handler_->GetWarningCount(); + } + } + + OFCondition Internals::getScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IGetRequestHandler& handler, + std::string remoteIp, + std::string remoteAet, + std::string calledAet, + int timeout) + { + GetScpData data; + data.lastRequest_ = NULL; + data.handler_ = &handler; + data.assoc_ = assoc; + data.remoteIp_ = remoteIp; + data.remoteAet_ = remoteAet; + data.calledAet_ = calledAet; + data.timeout_ = timeout; + + OFCondition cond = DIMSE_getProvider(assoc, presID, &msg->msg.CGetRQ, + GetScpCallback, &data, + /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + /*opt_dimse_timeout*/ timeout); + + // if some error occured, dump corresponding information and remove the outfile if necessary + if (cond.bad()) + { + OFString temp_str; + LOG(ERROR) << "Get SCP Failed: " << cond.text(); + } + + return cond; + } +} diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/DicomNetworking/Internals/GetScp.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/DicomNetworking/Internals/GetScp.h Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,51 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#include "../IGetRequestHandler.h" + +namespace Orthanc +{ + namespace Internals + { + OFCondition getScp(T_ASC_Association * assoc, + T_DIMSE_Message * msg, + T_ASC_PresentationContextID presID, + IGetRequestHandler& handler, + std::string remoteIp, + std::string remoteAet, + std::string calledAet, + int timeout); + } +} diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/Enumerations.cpp --- a/Core/Enumerations.cpp Wed May 20 16:42:44 2020 +0200 +++ b/Core/Enumerations.cpp Wed May 20 18:37:31 2020 +0200 @@ -369,6 +369,9 @@ case ErrorCode_AlreadyExistingTag: return "Cannot override the value of a tag that already exists"; + case ErrorCode_NoCGetHandler: + return "No request handler factory for DICOM C-GET SCP"; + case ErrorCode_NoStorageCommitmentHandler: return "No request handler factory for DICOM N-ACTION SCP (storage commitment)"; diff -r 6e14f2da7c7e -r 4d36d6e64c6d Core/Enumerations.h --- a/Core/Enumerations.h Wed May 20 16:42:44 2020 +0200 +++ b/Core/Enumerations.h Wed May 20 18:37:31 2020 +0200 @@ -241,6 +241,7 @@ ErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, ErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, ErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, + ErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, ErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, ErrorCode_START_PLUGINS = 1000000 }; diff -r 6e14f2da7c7e -r 4d36d6e64c6d NEWS --- a/NEWS Wed May 20 16:42:44 2020 +0200 +++ b/NEWS Wed May 20 18:37:31 2020 +0200 @@ -10,6 +10,7 @@ * New configuration options related to transcoding: "TranscodeDicomProtocol", "BuiltinDecoderTranscoderOrder", "IngestTranscoding" and "DicomLossyTranscodingQuality" +* Support of DICOM C-GET SCP (contribution by Varian) REST API -------- @@ -57,9 +58,9 @@ * Moved the GDCM sample plugin out of the Orthanc repository as a separate plugin * Fix missing body in "OrthancPluginHttpPost()" and "OrthancPluginHttpPut()" * Fix issue #169 (TransferSyntaxUID change from Explicit to Implicit during C-STORE SCU) +* Fix issue #179 (deadlock in Python plugins) * Upgraded dependencies for static builds (notably on Windows and LSB): - openssl 1.1.1g -* Fix issue #179 (deadlock in Python plugins) Version 1.6.1 (2020-04-21) diff -r 6e14f2da7c7e -r 4d36d6e64c6d OrthancServer/OrthancGetRequestHandler.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancGetRequestHandler.cpp Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,567 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#include "PrecompiledHeadersServer.h" +#include "OrthancGetRequestHandler.h" + +#include +#include +#include +#include +#include +#include + +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../Core/DicomFormat/DicomArray.h" +#include "../Core/Logging.h" +#include "../Core/MetricsRegistry.h" +#include "OrthancConfiguration.h" +#include "ServerContext.h" +#include "ServerJobs/DicomModalityStoreJob.h" + + +namespace Orthanc +{ + namespace + { + // Anonymous namespace to avoid clashes between compilation modules + + static void GetSubOpProgressCallback( + void * /* callbackData == pointer to the "OrthancGetRequestHandler" object */, + T_DIMSE_StoreProgress *progress, + T_DIMSE_C_StoreRQ * /*req*/) + { + // SBL - no logging to be done here. + } + } + + OrthancGetRequestHandler::Status + OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc) + { + if (position_ >= instances_.size()) + { + return Status_Failure; + } + + const std::string& id = instances_[position_++]; + + std::string dicom; + context_.ReadDicom(dicom, id); + + if (dicom.size() <= 0) + { + return Status_Failure; + } + + std::unique_ptr parsed( + FromDcmtkBridge::LoadFromMemoryBuffer(dicom.c_str(), dicom.size())); + + if (parsed.get() == NULL || + parsed->getDataset() == NULL) + { + throw OrthancException(ErrorCode_InternalError); + } + + DcmDataset& dataset = *parsed->getDataset(); + + OFString a, b; + if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() || + !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good()) + { + throw OrthancException(ErrorCode_NoSopClassOrInstance, + "Unable to determine the SOP class/instance for C-STORE with AET " + + originatorAet_); + } + + std::string sopClassUid(a.c_str()); + std::string sopInstanceUid(b.c_str()); + + OFCondition cond = PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release()); + + if (getCancelled_) + { + LOG(INFO) << "C-GET SCP: Received C-Cancel RQ"; + } + + if (cond.bad() || getCancelled_) + { + return Status_Failure; + } + + return Status_Success; + } + + + void OrthancGetRequestHandler::AddFailedUIDInstance(const std::string& sopInstance) + { + if (failedUIDs_.empty()) + { + failedUIDs_ = sopInstance; + } + else + { + failedUIDs_ += "\\" + sopInstance; + } + } + + + static bool SelectPresentationContext(T_ASC_PresentationContextID& selectedPresentationId, + DicomTransferSyntax& selectedSyntax, + T_ASC_Association* assoc, + const std::string& sopClassUid, + DicomTransferSyntax sourceSyntax, + bool allowTranscoding) + { + typedef std::map Accepted; + + Accepted accepted; + + /** + * 1. Inspect and index all the accepted transfer syntaxes. This + * is similar to the code from "DicomAssociation::Open()". + **/ + + LST_HEAD **l = &assoc->params->DULparams.acceptedPresentationContext; + if (*l != NULL) + { + DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); + LST_Position(l, (LST_NODE*)pc); + while (pc) + { + if (pc->result == ASC_P_ACCEPTANCE && + std::string(pc->abstractSyntax) == sopClassUid) + { + DicomTransferSyntax transferSyntax; + if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) + { + accepted[transferSyntax] = pc->presentationContextID; + } + else + { + LOG(WARNING) << "C-GET: Unknown transfer syntax received: " + << pc->acceptedTransferSyntax; + } + } + + pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); + } + } + + + /** + * 2. Select the preferred transfer syntaxes, which corresponds to + * the source transfer syntax, plus all the uncompressed transfer + * syntaxes if transcoding is enabled. + **/ + + std::list preferred; + preferred.push_back(sourceSyntax); + + if (allowTranscoding) + { + if (sourceSyntax != DicomTransferSyntax_LittleEndianImplicit) + { + // Default Transfer Syntax for DICOM + preferred.push_back(DicomTransferSyntax_LittleEndianImplicit); + } + + if (sourceSyntax != DicomTransferSyntax_LittleEndianExplicit) + { + preferred.push_back(DicomTransferSyntax_LittleEndianExplicit); + } + + if (sourceSyntax != DicomTransferSyntax_BigEndianExplicit) + { + // Retired + preferred.push_back(DicomTransferSyntax_BigEndianExplicit); + } + } + + + /** + * 3. Lookup whether one of the preferred transfer syntaxes was + * accepted. + **/ + + for (std::list::const_iterator + it = preferred.begin(); it != preferred.end(); ++it) + { + Accepted::const_iterator found = accepted.find(*it); + if (found != accepted.end()) + { + selectedPresentationId = found->second; + selectedSyntax = *it; + return true; + } + } + + // No preferred syntax was accepted + return false; + } + + + OFCondition OrthancGetRequestHandler::PerformGetSubOp(T_ASC_Association* assoc, + const std::string& sopClassUid, + const std::string& sopInstanceUid, + DcmFileFormat* dicomRaw) + { + assert(dicomRaw != NULL); + std::unique_ptr dicom(dicomRaw); + + DicomTransferSyntax sourceSyntax; + if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *dicom)) + { + nFailed_++; + AddFailedUIDInstance(sopInstanceUid); + LOG(ERROR) << "C-GET SCP: Unknown transfer syntax: (" + << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid; + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } + + bool allowTranscoding = (context_.IsTranscodeDicomProtocol() && + remote_.IsTranscodingAllowed()); + + T_ASC_PresentationContextID presId; + DicomTransferSyntax selectedSyntax; + if (!SelectPresentationContext(presId, selectedSyntax, assoc, sopClassUid, + sourceSyntax, allowTranscoding) || + presId == 0) + { + nFailed_++; + AddFailedUIDInstance(sopInstanceUid); + LOG(ERROR) << "C-GET SCP: storeSCU: No presentation context for: (" + << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid; + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } + else + { + // make sure that we can send images in this presentation context + T_ASC_PresentationContext pc; + ASC_findAcceptedPresentationContext(assoc->params, presId, &pc); + // the acceptedRole is the association requestor role + if ((pc.acceptedRole != ASC_SC_ROLE_SCP) && + (pc.acceptedRole != ASC_SC_ROLE_SCUSCP)) + { + // the role is not appropriate + nFailed_++; + AddFailedUIDInstance(sopInstanceUid); + LOG(ERROR) << "C-GET SCP: storeSCU: [No presentation context with requestor SCP role for: (" + << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid; + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } + } + + const DIC_US msgId = assoc->nextMsgID++; + + T_DIMSE_C_StoreRQ req; + memset(&req, 0, sizeof(req)); + req.MessageID = msgId; + strncpy(req.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN); + strncpy(req.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN); + req.DataSetType = DIMSE_DATASET_PRESENT; + req.Priority = DIMSE_PRIORITY_MEDIUM; + req.opts = 0; + + T_DIMSE_C_StoreRSP rsp; + memset(&rsp, 0, sizeof(rsp)); + + LOG(INFO) << "Store SCU RQ: MsgID " << msgId << ", (" + << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ")"; + + T_DIMSE_DetectedCancelParameters cancelParameters; + memset(&cancelParameters, 0, sizeof(cancelParameters)); + + std::unique_ptr stDetail; + + OFCondition cond; + + if (sourceSyntax == selectedSyntax) + { + // No transcoding is required + DcmDataset *stDetailTmp = NULL; + cond = DIMSE_storeUser( + assoc, presId, &req, NULL /* imageFileName */, dicom->getDataset(), + GetSubOpProgressCallback, this /* callbackData */, + (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_, + &rsp, &stDetailTmp, &cancelParameters); + stDetail.reset(stDetailTmp); + } + else + { + // Transcoding to the selected uncompressed transfer syntax + IDicomTranscoder::DicomImage source, transcoded; + source.AcquireParsed(dicom.release()); + + std::set ts; + ts.insert(selectedSyntax); + + if (context_.Transcode(transcoded, source, ts, true)) + { + // Transcoding has succeeded + DcmDataset *stDetailTmp = NULL; + cond = DIMSE_storeUser( + assoc, presId, &req, NULL /* imageFileName */, + transcoded.GetParsed().getDataset(), + GetSubOpProgressCallback, this /* callbackData */, + (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_, + &rsp, &stDetailTmp, &cancelParameters); + stDetail.reset(stDetailTmp); + } + else + { + // Cannot transcode + nFailed_++; + AddFailedUIDInstance(sopInstanceUid); + LOG(ERROR) << "C-GET SCP: Cannot transcode " << sopClassUid + << " from transfer syntax " << GetTransferSyntaxUid(sourceSyntax) + << " to " << GetTransferSyntaxUid(selectedSyntax); + return DIMSE_NOVALIDPRESENTATIONCONTEXTID; + } + } + + if (cond.good()) + { + if (cancelParameters.cancelEncountered) + { + if (origPresId_ == cancelParameters.presId && + origMsgId_ == cancelParameters.req.MessageIDBeingRespondedTo) + { + getCancelled_ = OFTrue; + } + else + { + LOG(ERROR) << "C-GET SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId + << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo; + } + } + + if (rsp.DimseStatus == STATUS_Success) + { + // everything ok + nCompleted_++; + } + else if ((rsp.DimseStatus & 0xf000) == 0xb000) + { + // a warning status message + warningCount_++; + LOG(ERROR) << "C-GET SCP: Store Warning: Response Status: " + << DU_cstoreStatusString(rsp.DimseStatus); + } + else + { + nFailed_++; + AddFailedUIDInstance(sopInstanceUid); + // print a status message + LOG(ERROR) << "C-GET SCP: Store Failed: Response Status: " + << DU_cstoreStatusString(rsp.DimseStatus); + } + } + else + { + nFailed_++; + AddFailedUIDInstance(sopInstanceUid); + OFString temp_str; + LOG(ERROR) << "C-GET SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond); + } + + if (stDetail.get() != NULL) + { + LOG(INFO) << " Status Detail:" << OFendl << DcmObject::PrintHelper(*stDetail); + } + + return cond; + } + + bool OrthancGetRequestHandler::LookupIdentifiers(std::list& publicIds, + ResourceType level, + const DicomMap& input) const + { + DicomTag tag(0, 0); // Dummy initialization + + switch (level) + { + case ResourceType_Patient: + tag = DICOM_TAG_PATIENT_ID; + break; + + case ResourceType_Study: + tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ? + DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID); + break; + + case ResourceType_Series: + tag = DICOM_TAG_SERIES_INSTANCE_UID; + break; + + case ResourceType_Instance: + tag = DICOM_TAG_SOP_INSTANCE_UID; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (!input.HasTag(tag)) + { + return false; + } + + const DicomValue& value = input.GetValue(tag); + if (value.IsNull() || + value.IsBinary()) + { + return false; + } + else + { + std::vector tokens; + Toolbox::TokenizeString(tokens, value.GetContent(), '\\'); + + for (size_t i = 0; i < tokens.size(); i++) + { + std::vector tmp; + context_.GetIndex().LookupIdentifierExact(tmp, level, tag, tokens[i]); + + if (tmp.empty()) + { + LOG(ERROR) << "C-GET: Cannot locate resource \"" << tokens[i] + << "\" at the " << EnumerationToString(level) << " level"; + return false; + } + else + { + for (size_t i = 0; i < tmp.size(); i++) + { + publicIds.push_back(tmp[i]); + } + } + } + + return true; + } + } + + + OrthancGetRequestHandler::OrthancGetRequestHandler(ServerContext& context) : + context_(context) + { + position_ = 0; + nRemaining_ = 0; + nCompleted_ = 0; + warningCount_ = 0; + nFailed_ = 0; + timeout_ = 0; + } + + + bool OrthancGetRequestHandler::Handle(const DicomMap& input, + const std::string& originatorIp, + const std::string& originatorAet, + const std::string& calledAet, + uint32_t timeout) + { + MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms"); + + LOG(WARNING) << "C-GET-SCU request received from AET \"" << originatorAet << "\""; + + { + DicomArray query(input); + for (size_t i = 0; i < query.GetSize(); i++) + { + if (!query.GetElement(i).GetValue().IsNull()) + { + LOG(INFO) << " " << query.GetElement(i).GetTag() + << " " << FromDcmtkBridge::GetTagName(query.GetElement(i)) + << " = " << query.GetElement(i).GetValue().GetContent(); + } + } + } + + /** + * Retrieve the query level. + **/ + + const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL); + + assert(levelTmp != NULL); + ResourceType level = StringToResourceType(levelTmp->GetContent().c_str()); + + + /** + * Lookup for the resource to be sent. + **/ + + std::list publicIds; + + if (!LookupIdentifiers(publicIds, level, input)) + { + LOG(ERROR) << "Cannot determine what resources are requested by C-GET"; + return false; + } + + localAet_ = context_.GetDefaultLocalApplicationEntityTitle(); + position_ = 0; + originatorAet_ = originatorAet; + + { + OrthancConfiguration::ReaderLock lock; + remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet); + } + + for (std::list::const_iterator + resource = publicIds.begin(); resource != publicIds.end(); ++resource) + { + LOG(INFO) << "C-GET: Sending resource " << *resource + << " to modality \"" << originatorAet << "\""; + + std::list tmp; + context_.GetIndex().GetChildInstances(tmp, *resource); + + instances_.reserve(tmp.size()); + for (std::list::iterator it = tmp.begin(); it != tmp.end(); ++it) + { + instances_.push_back(*it); + } + } + + failedUIDs_.clear(); + getCancelled_ = OFFalse; + + nRemaining_ = GetSubOperationCount(); + nCompleted_ = 0; + nFailed_ = 0; + warningCount_ = 0; + timeout_ = timeout; + + return true; + } +}; diff -r 6e14f2da7c7e -r 4d36d6e64c6d OrthancServer/OrthancGetRequestHandler.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/OrthancGetRequestHandler.h Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,122 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + +#pragma once + +#include "../Core/DicomNetworking/IGetRequestHandler.h" +#include "../Core/DicomNetworking/RemoteModalityParameters.h" + +#include + +#include + +class DcmFileFormat; + +namespace Orthanc +{ + class ServerContext; + + class OrthancGetRequestHandler : public IGetRequestHandler + { + private: + ServerContext& context_; + std::string localAet_; + std::vector instances_; + size_t position_; + RemoteModalityParameters remote_; + std::string originatorAet_; + + unsigned int nRemaining_; + unsigned int nCompleted_; + unsigned int warningCount_; + unsigned int nFailed_; + std::string failedUIDs_; + + DIC_US origMsgId_; + T_ASC_PresentationContextID origPresId_; + + bool getCancelled_; + uint32_t timeout_; + + bool LookupIdentifiers(std::list& publicIds, + ResourceType level, + const DicomMap& input) const; + + OFCondition PerformGetSubOp(T_ASC_Association *assoc, + const std::string& sopClassUid, + const std::string& sopInstanceUid, + DcmFileFormat* datasetRaw); + + void AddFailedUIDInstance(const std::string& sopInstance); + + public: + OrthancGetRequestHandler(ServerContext& context); + + virtual bool Handle(const DicomMap& input, + const std::string& originatorIp, + const std::string& originatorAet, + const std::string& calledAet, + uint32_t timeout) ORTHANC_OVERRIDE; + + virtual Status DoNext(T_ASC_Association *assoc) ORTHANC_OVERRIDE; + + virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE + { + return static_cast(instances_.size()); + } + + virtual unsigned int GetRemainingCount() const ORTHANC_OVERRIDE + { + return nRemaining_; + } + + virtual unsigned int GetCompletedCount() const ORTHANC_OVERRIDE + { + return nCompleted_; + } + + virtual unsigned int GetWarningCount() const ORTHANC_OVERRIDE + { + return warningCount_; + } + + virtual unsigned int GetFailedCount() const ORTHANC_OVERRIDE + { + return nFailed_; + } + + virtual const std::string& GetFailedUids() const ORTHANC_OVERRIDE + { + return failedUIDs_; + } + }; +} diff -r 6e14f2da7c7e -r 4d36d6e64c6d OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Wed May 20 16:42:44 2020 +0200 +++ b/OrthancServer/ServerContext.h Wed May 20 18:37:31 2020 +0200 @@ -483,5 +483,10 @@ DicomImage& source /* in, "GetParsed()" possibly modified */, const std::set& allowedSyntaxes, bool allowNewSopInstanceUid) ORTHANC_OVERRIDE; + + bool IsTranscodeDicomProtocol() const + { + return transcodeDicomProtocol_; + } }; } diff -r 6e14f2da7c7e -r 4d36d6e64c6d OrthancServer/main.cpp --- a/OrthancServer/main.cpp Wed May 20 16:42:44 2020 +0200 +++ b/OrthancServer/main.cpp Wed May 20 18:37:31 2020 +0200 @@ -51,6 +51,7 @@ #include "OrthancFindRequestHandler.h" #include "OrthancInitialization.h" #include "OrthancMoveRequestHandler.h" +#include "OrthancGetRequestHandler.h" #include "ServerContext.h" #include "ServerJobs/StorageCommitmentScpJob.h" #include "ServerToolbox.h" @@ -191,7 +192,8 @@ class MyDicomServerFactory : public IStoreRequestHandlerFactory, public IFindRequestHandlerFactory, - public IMoveRequestHandlerFactory, + public IMoveRequestHandlerFactory, + public IGetRequestHandlerFactory, public IStorageCommitmentRequestHandlerFactory { private: @@ -244,11 +246,17 @@ { return new OrthancMoveRequestHandler(context_); } - + + virtual IGetRequestHandler* ConstructGetRequestHandler() + { + return new OrthancGetRequestHandler(context_); + } + virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() { return new OrthancStorageCommitmentRequestHandler(context_); } + void Done() { @@ -761,6 +769,7 @@ PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series"); PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP"); PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists"); + PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP"); PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)"); PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type"); } @@ -1055,6 +1064,7 @@ dicomServer.SetRemoteModalities(modalities); dicomServer.SetStoreRequestHandlerFactory(serverFactory); dicomServer.SetMoveRequestHandlerFactory(serverFactory); + dicomServer.SetGetRequestHandlerFactory(serverFactory); dicomServer.SetFindRequestHandlerFactory(serverFactory); dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory); @@ -1139,7 +1149,7 @@ context.GetHttpHandler().Register(*plugins, false); } #endif - + // Secondly, apply the "static resources" layer #if ORTHANC_STANDALONE == 1 EmbeddedResourceHttpHandler staticResources("/app", EmbeddedResources::ORTHANC_EXPLORER); diff -r 6e14f2da7c7e -r 4d36d6e64c6d Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 20 16:42:44 2020 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 20 18:37:31 2020 +0200 @@ -309,6 +309,7 @@ OrthancPluginErrorCode_NoWorklistHandler = 2041 /*!< No request handler factory for DICOM C-Find Modality SCP */, OrthancPluginErrorCode_AlreadyExistingTag = 2042 /*!< Cannot override the value of a tag that already exists */, OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043 /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */, + OrthancPluginErrorCode_NoCGetHandler = 2044 /*!< No request handler factory for DICOM C-GET SCP */, OrthancPluginErrorCode_UnsupportedMediaType = 3000 /*!< Unsupported media type */, _OrthancPluginErrorCode_INTERNAL = 0x7fffffff diff -r 6e14f2da7c7e -r 4d36d6e64c6d Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed May 20 16:42:44 2020 +0200 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Wed May 20 18:37:31 2020 +0200 @@ -490,6 +490,7 @@ ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp + ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/GetScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/StoreScp.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp ${ORTHANC_ROOT}/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp diff -r 6e14f2da7c7e -r 4d36d6e64c6d Resources/Configuration.json --- a/Resources/Configuration.json Wed May 20 16:42:44 2020 +0200 +++ b/Resources/Configuration.json Wed May 20 18:37:31 2020 +0200 @@ -205,7 +205,7 @@ /** * By default, the Orthanc SCP accepts all DICOM commands (C-ECHO, - * C-STORE, C-FIND, C-MOVE, and storage commitment) issued by the + * C-STORE, C-FIND, C-MOVE, C-GET and storage commitment) issued by the * registered remote SCU modalities. Starting with Orthanc 1.5.0, * it is possible to specify which DICOM commands are allowed, * separately for each remote modality, using the syntax @@ -228,6 +228,7 @@ // "Manufacturer" : "Generic", // "AllowEcho" : false, // "AllowFind" : false, + // "AllowGet" : false, // "AllowMove" : false, // "AllowStore" : true, // "AllowStorageCommitment" : false, // new in 1.6.0 diff -r 6e14f2da7c7e -r 4d36d6e64c6d Resources/DcmtkTools/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/DcmtkTools/CMakeLists.txt Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,31 @@ +# $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../Resources/LinuxStandardBaseToolchain.cmake -G Ninja + +cmake_minimum_required(VERSION 2.8) + +project(DcmtkTools) + +set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../) +include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake) +include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake) + +set(STATIC_BUILD ON CACHE BOOL "") +set(ALLOW_DOWNLOADS ON CACHE BOOL "") + +set(DCMTK_STATIC_VERSION "3.6.5" CACHE STRING "") +set(ENABLE_DCMTK_JPEG ON CACHE BOOL "") +set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "") +set(ENABLE_DCMTK_LOG ON CACHE BOOL "") +set(ENABLE_DCMTK_NETWORKING ON CACHE BOOL "") +set(ENABLE_DCMTK_TRANSCODING ON CACHE BOOL "") + +include(${ORTHANC_ROOT}/Resources/CMake/DcmtkConfiguration.cmake) + +add_library(dcmtk STATIC + ${CMAKE_SOURCE_DIR}/dummy.cpp + ${DCMTK_SOURCES} + ) + +add_executable(getscu + ${DCMTK_SOURCES_DIR}/dcmnet/apps/getscu.cc + ) +target_link_libraries(getscu dcmtk) diff -r 6e14f2da7c7e -r 4d36d6e64c6d Resources/DcmtkTools/dummy.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/DcmtkTools/dummy.cpp Wed May 20 18:37:31 2020 +0200 @@ -0,0 +1,22 @@ +#include + +struct OrthancLinesIterator; + +OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content) +{ + return NULL; +} + +bool OrthancLinesIterator_GetLine(std::string& target, + const OrthancLinesIterator* iterator) +{ + return false; +} + +void OrthancLinesIterator_Next(OrthancLinesIterator* iterator) +{ +} + +void OrthancLinesIterator_Free(OrthancLinesIterator* iterator) +{ +} diff -r 6e14f2da7c7e -r 4d36d6e64c6d Resources/DicomConformanceStatement.txt --- a/Resources/DicomConformanceStatement.txt Wed May 20 16:42:44 2020 +0200 +++ b/Resources/DicomConformanceStatement.txt Wed May 20 18:37:31 2020 +0200 @@ -162,6 +162,16 @@ MOVEStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.2 +------------------- +Get SCP Conformance +------------------- + +Orthanc supports the following SOP Classes as an SCP for C-Get: + + GETPatientRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.1.3 + GETStudyRootQueryRetrieveInformationModel | 1.2.840.10008.5.1.4.1.2.2.3 + + --------------------- Echo SCU Conformance --------------------- diff -r 6e14f2da7c7e -r 4d36d6e64c6d Resources/ErrorCodes.json --- a/Resources/ErrorCodes.json Wed May 20 16:42:44 2020 +0200 +++ b/Resources/ErrorCodes.json Wed May 20 18:37:31 2020 +0200 @@ -557,6 +557,11 @@ "Name": "NoStorageCommitmentHandler", "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)" }, + { + "Code": 2044, + "Name": "NoCGetHandler", + "Description": "No request handler factory for DICOM C-GET SCP" + }