changeset 6370:7c8e00ea7830

Move SCU jobs details
author Alain Mazy <am@orthanc.team>
date Fri, 07 Nov 2025 18:28:39 +0100
parents 99f6accf4270
children e75817843360
files NEWS OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h OrthancServer/Sources/main.cpp
diffstat 11 files changed, 108 insertions(+), 73 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Nov 07 15:02:25 2025 +0100
+++ b/NEWS	Fri Nov 07 18:28:39 2025 +0100
@@ -15,7 +15,7 @@
   actually failed.
 * C-Store, C-Move and C-Get jobs and HTTP responses now include a new "DimseErrorStatus" 
   field (ONLY if the operation fails).
-* C-Get jobs and HTTP responses now include a new "Details" field with the "DimseErrorStatus"
+* C-Move and C-Get jobs and HTTP responses now include a new "Details" field with the "DimseErrorStatus"
   of each operation and the list of "RetrievedInstancesIds".  Note that, if you have multiple
   resources to retrieve and one of them fails, you'll only get those details if you have set
   "Permissive" to true to allow other operations to continue after the failure.
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Fri Nov 07 18:28:39 2025 +0100
@@ -426,7 +426,8 @@
     
   void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
                                                 ResourceType level,
-                                                const DicomMap& fields)
+                                                const DicomMap& fields,
+                                                uint16_t messageId)
   {
     assert(association_.get() != NULL);
     association_->Open(parameters_);
@@ -448,7 +449,15 @@
 
     T_DIMSE_C_MoveRQ request;
     memset(&request, 0, sizeof(request));
-    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    if (messageId == 0)
+    {
+      request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    }
+    else
+    {
+      request.MessageID = messageId;
+    }
+    
     strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
     request.Priority = DIMSE_PRIORITY_MEDIUM;
     request.DataSetType = DIMSE_DATASET_PRESENT;
@@ -907,7 +916,8 @@
 
   void DicomControlUserConnection::Move(const std::string& targetAet,
                                         ResourceType level,
-                                        const DicomMap& moveQuery)
+                                        const DicomMap& moveQuery,
+                                        uint16_t messageId)
   {
     DicomMap move;
     switch (level)
@@ -935,12 +945,13 @@
         throw OrthancException(ErrorCode_InternalError);
     }
 
-    MoveInternal(targetAet, level, move);
+    MoveInternal(targetAet, level, move, messageId);
   }
 
 
   void DicomControlUserConnection::Move(const std::string& targetAet,
-                                        const DicomMap& moveQuery)
+                                        const DicomMap& moveQuery,
+                                        uint16_t messageId)
   {
     if (!moveQuery.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
     {
@@ -950,49 +961,7 @@
     const std::string tmp = moveQuery.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
     ResourceType level = StringToResourceType(tmp.c_str());
 
-    Move(targetAet, level, moveQuery);
-  }
-
-
-  void DicomControlUserConnection::MovePatient(const std::string& targetAet,
-                                               const std::string& patientId)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
-    MoveInternal(targetAet, ResourceType_Patient, query);
-  }
-    
-
-  void DicomControlUserConnection::MoveStudy(const std::string& targetAet,
-                                             const std::string& studyUid)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
-    MoveInternal(targetAet, ResourceType_Study, query);
-  }
-
-    
-  void DicomControlUserConnection::MoveSeries(const std::string& targetAet,
-                                              const std::string& studyUid,
-                                              const std::string& seriesUid)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
-    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
-    MoveInternal(targetAet, ResourceType_Series, query);
-  }
-
-
-  void DicomControlUserConnection::MoveInstance(const std::string& targetAet,
-                                                const std::string& studyUid,
-                                                const std::string& seriesUid,
-                                                const std::string& instanceUid)
-  {
-    DicomMap query;
-    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
-    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
-    query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
-    MoveInternal(targetAet, ResourceType_Instance, query);
+    Move(targetAet, level, moveQuery, messageId);
   }
 
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Fri Nov 07 18:28:39 2025 +0100
@@ -90,7 +90,8 @@
     
     void MoveInternal(const std::string& targetAet,
                       ResourceType level,
-                      const DicomMap& fields);
+                      const DicomMap& fields,
+                      uint16_t messageId);
     
   public:
     explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation);
@@ -126,26 +127,13 @@
 
     void Move(const std::string& targetAet,
               ResourceType level,
-              const DicomMap& moveQuery);
+              const DicomMap& moveQuery,
+              uint16_t messageId = 0);
     
     void Move(const std::string& targetAet,
-              const DicomMap& moveQuery);
+              const DicomMap& moveQuery,
+              uint16_t messageId = 0);
     
-    void MovePatient(const std::string& targetAet,
-                     const std::string& patientId);
-
-    void MoveStudy(const std::string& targetAet,
-                   const std::string& studyUid);
-
-    void MoveSeries(const std::string& targetAet,
-                    const std::string& studyUid,
-                    const std::string& seriesUid);
-
-    void MoveInstance(const std::string& targetAet,
-                      const std::string& studyUid,
-                      const std::string& seriesUid,
-                      const std::string& instanceUid);
-
     void FindWorklist(DicomFindAnswers& result,
                       ParsedDicomFile& query);
   };
--- a/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Fri Nov 07 18:28:39 2025 +0100
@@ -44,6 +44,8 @@
     virtual uint16_t Handle(DcmDataset& dicom,
                             const std::string& remoteIp,
                             const std::string& remoteAet,
-                            const std::string& calledAet) = 0;
+                            const std::string& calledAet,
+                            uint16_t originatorMessageId,
+                            const std::string& originatorAet) = 0;
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Fri Nov 07 18:28:39 2025 +0100
@@ -184,7 +184,7 @@
               {
                 try
                 {
-                  rsp->DimseStatus = cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET);
+                  rsp->DimseStatus = cbdata->handler->Handle(**imageDataSet, *cbdata->remoteIp, cbdata->remoteAET, cbdata->calledAET, req->MoveOriginatorID, req->MoveOriginatorApplicationEntityTitle);
                 }
                 catch (OrthancException& e)
                 {
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.cpp	Fri Nov 07 18:28:39 2025 +0100
@@ -36,7 +36,9 @@
 uint16_t StoreRequestHandler::Handle(DcmDataset& dicom,
                                      const std::string& remoteIp,
                                      const std::string& remoteAet,
-                                     const std::string& calledAet)
+                                     const std::string& calledAet,
+                                     uint16_t originatorMessageId,
+                                     const std::string& originatorAet)
 {
   std::string buffer;
   std::string errorMessage;
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/StoreRequestHandler.h	Fri Nov 07 18:28:39 2025 +0100
@@ -44,5 +44,7 @@
   virtual uint16_t Handle(DcmDataset& dicom,
                           const std::string& remoteIp,
                           const std::string& remoteAet,
-                          const std::string& calledAet) ORTHANC_OVERRIDE;
+                          const std::string& calledAet,
+                          uint16_t originatorMessageId,
+                          const std::string& originatorAet) ORTHANC_OVERRIDE;
 };
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Fri Nov 07 18:28:39 2025 +0100
@@ -47,7 +47,9 @@
     }
     
     connection_->SetProgressListener(this);
-    connection_->Move(targetAet_, findAnswer);
+    
+    // we use a unique message ID in order to know to which Move request a stored instance relates to.
+    connection_->Move(targetAet_, findAnswer, GetMessageId(GetParameters().GetLocalApplicationEntityTitle()));
   }
 
 
--- a/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp	Fri Nov 07 18:28:39 2025 +0100
@@ -29,6 +29,7 @@
 #include <dcmtk/dcmnet/dimse.h>
 #include <algorithm>
 #include "../../../OrthancFramework/Sources/Logging.h"
+#include <boost/thread/mutex.hpp>
 
 static const char* const LOCAL_AET = "LocalAet";
 static const char* const QUERY = "Query";
@@ -36,8 +37,59 @@
 static const char* const REMOTE = "Remote";
 static const char* const TIMEOUT = "Timeout";
 
+static std::map<std::string, Orthanc::SetOfCommandsJob::ICommand*> messagesRegistry; 
+static uint16_t messageRegistryCurrentId = 1000;
+static boost::mutex messageRegistryMutex;
+
 namespace Orthanc
 {
+  std::string GetKey(const std::string& aet, uint16_t messageId)
+  {
+    return aet + "-" + boost::lexical_cast<std::string>(messageId);
+  }
+
+  uint16_t DicomRetrieveScuBaseJob::GetMessageId(const std::string& localAet)
+  {
+    assert(currentCommand_ != NULL);
+    boost::mutex::scoped_lock lock(messageRegistryMutex);
+    
+    // Each resource retrieval (command) has its own messageId.  
+    // We start at 1000 to clearly differentiate them from other messages.  We can actually use ANY value between 0 & 65535.
+    messageRegistryCurrentId = std::max(1000, (messageRegistryCurrentId + 1) % 0xFFFF);
+    messagesRegistry[GetKey(localAet, messageRegistryCurrentId)] = currentCommand_;
+    
+    return messageRegistryCurrentId;
+  }
+
+  void DicomRetrieveScuBaseJob::AddReceivedInstanceFromCStore(uint16_t originatorMessageId, 
+                                                              const std::string& originatorAet, 
+                                                              const std::string& instanceId)
+  {
+    boost::mutex::scoped_lock lock(messageRegistryMutex);
+
+    std::string key = GetKey(originatorAet, originatorMessageId);
+
+    if (messagesRegistry.find(key) != messagesRegistry.end())
+    {
+      dynamic_cast<DicomRetrieveScuBaseJob::Command*>(messagesRegistry[key])->AddReceivedInstance(instanceId);
+    }
+  }
+
+  DicomRetrieveScuBaseJob::Command::~Command()
+  {
+    // remove the command from the messageRegistry
+    boost::mutex::scoped_lock lock(messageRegistryMutex);
+
+    for (std::map<std::string, Orthanc::SetOfCommandsJob::ICommand*>::const_iterator it = messagesRegistry.begin();
+         it != messagesRegistry.end(); ++it)
+    {
+      if (it->second == this)
+      {
+        messagesRegistry.erase(it);
+        return;
+      }
+    }
+  }
 
   bool DicomRetrieveScuBaseJob::Command::Execute(const std::string &jobId)
   {
--- a/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h	Fri Nov 07 18:28:39 2025 +0100
@@ -36,6 +36,8 @@
   class DicomRetrieveScuBaseJob : public SetOfCommandsJob, public DicomControlUserConnection::IProgressListener
   {
   protected:
+    uint16_t GetMessageId(const std::string& localAet);
+    
     class Command : public SetOfCommandsJob::ICommand
     {
     private:
@@ -54,6 +56,8 @@
       {
       }
 
+      virtual ~Command();
+
       virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE;
 
       virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE;
@@ -165,5 +169,9 @@
     }
 
     void AddReceivedInstance(const std::string& instanceId);
+
+    static void AddReceivedInstanceFromCStore(uint16_t originatorMessageId, 
+                                              const std::string& originatorAet, 
+                                              const std::string& instanceId);
   };
 }
--- a/OrthancServer/Sources/main.cpp	Fri Nov 07 15:02:25 2025 +0100
+++ b/OrthancServer/Sources/main.cpp	Fri Nov 07 18:28:39 2025 +0100
@@ -47,6 +47,7 @@
 #include "ServerContext.h"
 #include "ServerEnumerations.h"
 #include "ServerJobs/StorageCommitmentScpJob.h"
+#include "ServerJobs/DicomRetrieveScuBaseJob.h"
 #include "ServerToolbox.h"
 #include "StorageCommitmentReports.h"
 
@@ -86,7 +87,9 @@
   virtual uint16_t Handle(DcmDataset& dicom,
                           const std::string& remoteIp,
                           const std::string& remoteAet,
-                          const std::string& calledAet) ORTHANC_OVERRIDE 
+                          const std::string& calledAet,
+                          uint16_t originatorMessageId,
+                          const std::string& originatorAet) ORTHANC_OVERRIDE 
   {
     std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dicom));
     
@@ -97,6 +100,13 @@
 
       std::string id;
       ServerContext::StoreResult result = context_.Store(id, *toStore, StoreInstanceMode_Default);
+
+      if (result.GetStatus() == StoreStatus_Success || result.GetStatus() == StoreStatus_AlreadyStored)
+      {
+        // In case this C-Store was triggered from a C-Move job, keep track of the instances that have been received
+        DicomRetrieveScuBaseJob::AddReceivedInstanceFromCStore(originatorMessageId, originatorAet, id);
+      }
+
       return result.GetCStoreStatusCode();
     }