changeset 6211:55b41faaaec3

SDK: OrthancPluginSetStableStatus
author Alain Mazy <am@orthanc.team>
date Wed, 25 Jun 2025 14:14:47 +0200
parents 6300c3adcb07
children 3a974dbf4740
files NEWS OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Samples/Basic/Plugin.c OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h
diffstat 9 files changed, 247 insertions(+), 49 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Jun 16 21:31:01 2025 +0200
+++ b/NEWS	Wed Jun 25 14:14:47 2025 +0200
@@ -1,6 +1,13 @@
 Pending changes in the mainline
 ===============================
 
+Plugin SDK
+----------
+
+* Added new function OrthancPluginSetStableStatus to e.g force the 
+  stabilization of a study from a plugin.
+
+
 Plugins
 -------
 
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Jun 25 14:14:47 2025 +0200
@@ -4703,6 +4703,24 @@
     *parameters.size = lock.GetContext().GetIndex().GetQueueSize(parameters.queueId);
   }
 
+  void OrthancPlugins::ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters)
+  {
+    PImpl::ServerContextReference lock(*pimpl_);
+    bool statusHasChanged = false;
+
+    lock.GetContext().GetIndex().SetStableStatus(statusHasChanged,
+                                                 parameters.resourceId,
+                                                 (parameters.stableStatus == OrthancPluginStableStatus_Stable));
+    if (statusHasChanged)
+    {
+      *(parameters.statusHasChanged) = 1;
+    }
+    else
+    {
+      *(parameters.statusHasChanged) = 0;
+    }
+  }
+
   void OrthancPlugins::ApplyLoadDicomInstance(const _OrthancPluginLoadDicomInstance& params)
   {
     std::unique_ptr<IDicomInstance> target;
@@ -5826,6 +5844,13 @@
         return true;
       }
 
+      case _OrthancPluginService_SetStableStatus:
+      {
+        const _OrthancPluginSetStableStatus& p = *reinterpret_cast<const _OrthancPluginSetStableStatus*>(parameters);
+        ApplySetStableStatus(p);
+        return true;
+      }
+
       default:
         return false;
     }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.h	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed Jun 25 14:14:47 2025 +0200
@@ -244,6 +244,8 @@
 
     void ApplyGetQueueSize(const _OrthancPluginGetQueueSize& parameters);
 
+    void ApplySetStableStatus(const _OrthancPluginSetStableStatus& parameters);
+
     void ComputeHash(_OrthancPluginService service,
                      const void* parameters);
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jun 25 14:14:47 2025 +0200
@@ -121,7 +121,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  8
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  9
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -483,6 +483,7 @@
     _OrthancPluginService_EnqueueValue = 57,                        /* New in Orthanc 1.12.8 */
     _OrthancPluginService_DequeueValue = 58,                        /* New in Orthanc 1.12.8 */
     _OrthancPluginService_GetQueueSize = 59,                        /* New in Orthanc 1.12.8 */
+    _OrthancPluginService_SetStableStatus = 60,                     /* New in Orthanc 1.12.9 */
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -1169,6 +1170,16 @@
     _OrthancPluginQueueOrigin_INTERNAL = 0x7fffffff
   } OrthancPluginQueueOrigin;
 
+  /**
+   * The "stable" status of a resource.
+   **/
+  typedef enum
+  {
+    OrthancPluginStableStatus_Stable = 0,     /*!< The resource is stable */
+    OrthancPluginStableStatus_Unstable = 1,   /*!< The resource is unstable */
+
+    _OrthancPluginStableStatus_INTERNAL = 0x7fffffff
+  } OrthancPluginStableStatus;
 
 
   /**
@@ -9969,6 +9980,7 @@
     return context->InvokeService(context, _OrthancPluginService_StoreKeyValue, &params);
   }
 
+
   typedef struct
   {
     const char*                   storeId;
@@ -9995,6 +10007,7 @@
     return context->InvokeService(context, _OrthancPluginService_DeleteKeyValue, &params);
   }
 
+
   typedef struct
   {
     uint8_t*                      found;
@@ -10039,7 +10052,6 @@
   typedef struct _OrthancPluginKeysValuesIterator_t OrthancPluginKeysValuesIterator;
 
 
-
   typedef struct
   {
     OrthancPluginKeysValuesIterator**  target;
@@ -10197,7 +10209,6 @@
   }
 
 
-
   typedef struct
   {
     const char*                   queueId;
@@ -10228,6 +10239,7 @@
     return context->InvokeService(context, _OrthancPluginService_EnqueueValue, &params);
   }
 
+
   typedef struct
   {
     uint8_t*                      found;
@@ -10263,6 +10275,7 @@
     return context->InvokeService(context, _OrthancPluginService_DequeueValue, &params);
   }
 
+
   typedef struct
   {
     const char*                   queueId;
@@ -10289,6 +10302,43 @@
     return context->InvokeService(context, _OrthancPluginService_GetQueueSize, &params);
   }
 
+
+  typedef struct
+  {
+    const char*                   resourceId;
+    OrthancPluginStableStatus     stableStatus;
+    int32_t*                      statusHasChanged;
+  } _OrthancPluginSetStableStatus;
+
+  /**
+   * @brief Change the "Stable" status of a resource.  
+   *        Forcing a resource to "Stable" if it is currently "Unstable" will change 
+   *        its Stable status AND trigger a new Stable change which will also trigger 
+   *        listener callbacks.
+   *        Forcing a resource to "Stable" if it is already "Stable" is a no-op.
+   *        Forcing a resource to "Unstable" will change its Stable status to "Unstable"
+   *        AND reset its stabilization period, no matter of its initial state.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param statusHasChanged Wheter the status has changed (1) or not (0) during the execution of this command.
+   * @param resourceId The Orthanc identifier of the DICOM resource of interest.
+   * @param stableStatus The new stable status of the resource of interest.
+   * @return 0 if success, other value if error.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetStableStatus(
+    OrthancPluginContext*         context,
+    int32_t*                      statusHasChanged,  /* out */
+    const char*                   resourceId,  /* in */
+    OrthancPluginStableStatus     stableStatus /* in */)
+  {
+    _OrthancPluginSetStableStatus params;
+    params.resourceId = resourceId;
+    params.stableStatus= stableStatus;
+    params.statusHasChanged = statusHasChanged;
+
+    return context->InvokeService(context, _OrthancPluginService_SetStableStatus, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Samples/Basic/Plugin.c	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c	Wed Jun 25 14:14:47 2025 +0200
@@ -226,6 +226,44 @@
 }
 
 
+
+OrthancPluginErrorCode CallbackStabilizeStudy(OrthancPluginRestOutput* output,
+                                              const char* url,
+                                              const OrthancPluginHttpRequest* request)
+{
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+  }
+  else
+  {
+    const char* studyId = request->groups[0];
+    int32_t statusHasChanged = 0;
+
+    if (strcmp(request->groups[1], "stabilize") == 0)
+    {
+      OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Stable);
+    }
+    else
+    {
+      OrthancPluginSetStableStatus(context, &statusHasChanged, studyId, OrthancPluginStableStatus_Unstable);
+    }
+
+    if (statusHasChanged)
+    {
+      OrthancPluginAnswerBuffer(context, output, "CHANGED\n", 8, "text/plain");
+    }
+    else
+    {
+      OrthancPluginAnswerBuffer(context, output, "UNCHANGED\n", 10, "text/plain");
+    }
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
 OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output,
                                            const char* url,
                                            const OrthancPluginHttpRequest* request)
@@ -572,6 +610,7 @@
   OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5);
   OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
   OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/dicom-web", CallbackDicomWeb);
+  OrthancPluginRegisterRestCallback(context, "/studies/([^/]+)/(stabilize|unstabilize)", CallbackStabilizeStudy);
 
   OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Jun 25 14:14:47 2025 +0200
@@ -1394,28 +1394,36 @@
   }
 
 
-  bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type,
-                                                       const std::string& publicId)
+  bool StatelessDatabaseOperations::LookupResource(int64_t& internalId,
+                                                   ResourceType& type,
+                                                   const std::string& publicId)
   {
-    class Operations : public ReadOnlyOperationsT3<bool&, ResourceType&, const std::string&>
+    class Operations : public ReadOnlyOperationsT4<bool&, int64_t&, ResourceType&, const std::string&>
     {
     public:
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
                               const Tuple& tuple) ORTHANC_OVERRIDE
       {
         // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        int64_t id;
-        tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>());
+        tuple.get<0>() = transaction.LookupResource(tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
       }
     };
 
     bool found;
     Operations operations;
-    operations.Apply(*this, found, type, publicId);
+    operations.Apply(*this, found, internalId, type, publicId);
     return found;
   }
 
 
+  bool StatelessDatabaseOperations::LookupResourceType(ResourceType& type,
+                                                       const std::string& publicId)
+  {
+    int64_t internalId;
+    return LookupResource(internalId, type, publicId);
+  }
+
+
   bool StatelessDatabaseOperations::LookupParent(std::string& target,
                                                  const std::string& publicId,
                                                  ResourceType parentType)
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Jun 25 14:14:47 2025 +0200
@@ -691,6 +691,10 @@
     bool GetAllMainDicomTags(DicomMap& result,
                              const std::string& instancePublicId);
 
+    bool LookupResource(int64_t& id,
+                        ResourceType& type,
+                        const std::string& publicId);
+
     bool LookupResourceType(ResourceType& type,
                             const std::string& publicId);
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Sources/ServerIndex.cpp	Wed Jun 25 14:14:47 2025 +0200
@@ -312,7 +312,7 @@
   bool ServerIndex::IsUnstableResource(ResourceType type,
                                        int64_t id)
   {
-    boost::mutex::scoped_lock lock(monitoringMutex_);
+    boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
     return unstableResources_.Contains(std::make_pair(type, id));
   }
 
@@ -394,7 +394,7 @@
   void ServerIndex::SetMaximumPatientCount(unsigned int count) 
   {
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumPatients_ = count;
       
       if (count == 0)
@@ -414,7 +414,7 @@
   void ServerIndex::SetMaximumStorageSize(uint64_t size) 
   {
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize_ = size;
       
       if (size == 0)
@@ -433,7 +433,7 @@
   void ServerIndex::SetMaximumStorageMode(MaxStorageMode mode) 
   {
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageMode_ = mode;
       
       if (mode == MaxStorageMode_Recycle)
@@ -486,7 +486,7 @@
         int64_t stableId;
 
         {      
-          boost::mutex::scoped_lock lock(that->monitoringMutex_);
+          boost::recursive_mutex::scoped_lock lock(that->monitoringMutex_);
 
           if (!that->unstableResources_.IsEmpty() &&
               that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
@@ -505,43 +505,52 @@
           }
         }
 
-        try
-        {
-          /**
-           * WARNING: Don't protect the calls to "LogChange()" using
-           * "monitoringMutex_", as this could lead to deadlocks in
-           * other threads (typically, if "Store()" is being running in
-           * another thread, which leads to calls to "MarkAsUnstable()",
-           * which leads to two lockings of "monitoringMutex_").
-           **/
-          switch (stableLevel)
-          {
-            case ResourceType_Patient:
-              that->LogChange(stableId, ChangeType_StablePatient, stablePayload.GetPublicId(), ResourceType_Patient);
-              break;
-            
-            case ResourceType_Study:
-              that->LogChange(stableId, ChangeType_StableStudy, stablePayload.GetPublicId(), ResourceType_Study);
-              break;
-            
-            case ResourceType_Series:
-              that->LogChange(stableId, ChangeType_StableSeries, stablePayload.GetPublicId(), ResourceType_Series);
-              break;
-            
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        catch (OrthancException& e)
-        {
-          LOG(ERROR) << "Cannot log a change about a stable resource into the database";
-        }          
+        // must not be protected by monitoringMutex_
+        that->LogStableChange(stableLevel, stableId, stablePayload.GetPublicId());
       }
     }
 
     LOG(INFO) << "Closing the monitor thread for stable resources";
   }
   
+  void ServerIndex::LogStableChange(ResourceType stableLevel,
+                                    int64_t stableId,
+                                    const std::string& publicId)
+  {
+    try
+    {
+      /**
+        * WARNING: Don't protect the calls to "LogChange()" using
+        * "monitoringMutex_", as this could lead to deadlocks in
+        * other threads (typically, if "Store()" is being running in
+        * another thread, which leads to calls to "MarkAsUnstable()",
+        * which leads to two lockings of "monitoringMutex_").
+        **/
+      switch (stableLevel)
+      {
+        case ResourceType_Patient:
+          LogChange(stableId, ChangeType_StablePatient, publicId, ResourceType_Patient);
+          break;
+        
+        case ResourceType_Study:
+          LogChange(stableId, ChangeType_StableStudy, publicId, ResourceType_Study);
+          break;
+        
+        case ResourceType_Series:
+          LogChange(stableId, ChangeType_StableSeries, publicId, ResourceType_Series);
+          break;
+        
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Cannot log a change about a stable resource into the database";
+    }          
+  }
+
+
 
   void ServerIndex::MarkAsUnstable(ResourceType type,
                                    int64_t id,
@@ -552,13 +561,59 @@
            type == ResourceType_Series);
 
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       UnstableResourcePayload payload(publicId);
       unstableResources_.AddOrMakeMostRecent(std::make_pair(type, id), payload);
       //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
     }
   }
 
+  bool ServerIndex::SetStableStatus(bool& statusHasChanged,
+                                    const std::string& resourceId,
+                                    bool setNewStatusToStable)
+  {
+    int64_t id;
+    ResourceType type;
+
+    if (LookupResource(id, type, resourceId))
+    {
+      if (setNewStatusToStable)
+      {
+        {
+          boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
+
+          if (IsUnstableResource(type, id))
+          {
+            unstableResources_.Invalidate(std::pair<ResourceType, int64_t>(type, id));
+            statusHasChanged = true;
+          }
+        }
+
+        if (statusHasChanged)
+        {
+          // must not be protected by monitoringMutex_
+          LogStableChange(type, id, resourceId);
+        }
+      }
+      else
+      {
+        {
+          boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
+
+          statusHasChanged = !IsUnstableResource(type, id);
+
+          // no matter what was the status, we mark it as unstable to reset its stabilization period
+          MarkAsUnstable(type, id, resourceId);
+        }
+
+      }
+
+      return true;
+    }
+
+    return false;
+  }
+
 
   StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
                                  const DicomMap& dicomSummary,
@@ -578,7 +633,7 @@
     MaxStorageMode maximumStorageMode;
     
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize = maximumStorageSize_;
       maximumPatients = maximumPatients_;
       maximumStorageMode = maximumStorageMode_;
@@ -602,7 +657,7 @@
     unsigned int maximumPatients;
     
     {
-      boost::mutex::scoped_lock lock(monitoringMutex_);
+      boost::recursive_mutex::scoped_lock lock(monitoringMutex_);
       maximumStorageSize = maximumStorageSize_;
       maximumPatients = maximumPatients_;
     }
--- a/OrthancServer/Sources/ServerIndex.h	Mon Jun 16 21:31:01 2025 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Wed Jun 25 14:14:47 2025 +0200
@@ -40,7 +40,7 @@
     class UnstableResourcePayload;
 
     bool done_;
-    boost::mutex monitoringMutex_;
+    boost::recursive_mutex monitoringMutex_;
     boost::thread flushThread_;
     boost::thread unstableResourcesMonitorThread_;
 
@@ -61,6 +61,10 @@
                         int64_t id,
                         const std::string& publicId);
 
+    void LogStableChange(ResourceType type,
+                         int64_t id,
+                         const std::string& publicId);
+
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
@@ -101,5 +105,9 @@
 
     bool IsUnstableResource(ResourceType type,
                             int64_t id);
+
+    bool SetStableStatus(bool& statusHasChanged,
+                         const std::string& resourceId,
+                         bool setNewStatusToStable);
   };
 }