changeset 5953:684d49e47b5e default tip

added OrthancPluginStartStreamAnswer and OrthancPluginSendStreamChunk to allow sending HTTP response by chunks
author Alain Mazy <am@orthanc.team>
date Fri, 10 Jan 2025 18:27:27 +0100
parents bfadfbcca13e
children
files NEWS OrthancFramework/Sources/HttpServer/HttpOutput.cpp OrthancFramework/Sources/HttpServer/HttpOutput.h OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h
diffstat 5 files changed, 200 insertions(+), 26 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Jan 08 15:18:00 2025 +0100
+++ b/NEWS	Fri Jan 10 18:27:27 2025 +0100
@@ -15,6 +15,13 @@
   the SOPInstanceUID untouched in this case.
 
 
+Plugins
+-------
+
+* SDK: added OrthancPluginStartStreamAnswer and OrthancPluginSendStreamChunk to allow
+  sending HTTP response by chunks.
+
+
 Version 1.12.5 (2024-12-17)
 ===========================
 
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Wed Jan 08 15:18:00 2025 +0100
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Fri Jan 10 18:27:27 2025 +0100
@@ -474,6 +474,10 @@
     return stateMachine_.GetState() == StateMachine::State_WritingMultipart;
   }
 
+  bool HttpOutput::IsWritingStream() const
+  {
+    return stateMachine_.GetState() == StateMachine::State_WritingStream;
+  }
   
   void HttpOutput::Answer(const void* buffer,
                           size_t length)
@@ -977,4 +981,21 @@
 
     stateMachine_.CloseStream();
   }
+
+  void HttpOutput::StartStream(const std::string& contentType)
+  {
+    stateMachine_.StartStream(contentType.c_str());
+  }
+
+  void HttpOutput::SendStreamItem(const void* data,
+                                  size_t size)
+  {
+    stateMachine_.SendStreamItem(data, size);
+  }
+
+  void HttpOutput::CloseStream()
+  {
+    stateMachine_.CloseStream();
+  }
+
 }
--- a/OrthancFramework/Sources/HttpServer/HttpOutput.h	Wed Jan 08 15:18:00 2025 +0100
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Fri Jan 10 18:27:27 2025 +0100
@@ -224,5 +224,14 @@
      * used to handle compression using "Content-Encoding".
      **/
     void AnswerWithoutBuffering(IHttpStreamAnswer& stream);
+
+    void StartStream(const std::string& contentType);
+
+    void SendStreamItem(const void* data,
+                        size_t size);
+
+    void CloseStream();
+
+    bool IsWritingStream() const;
   };
 }
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Jan 08 15:18:00 2025 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Fri Jan 10 18:27:27 2025 +0100
@@ -1101,40 +1101,67 @@
     class PluginHttpOutput : public boost::noncopyable
     {
     private:
-      enum MultipartState
-      {
-        MultipartState_None,
-        MultipartState_FirstPart,
-        MultipartState_SecondPart,
-        MultipartState_NextParts
+      // enum MultipartState
+      // {
+      //   MultipartState_None,
+      //   MultipartState_FirstPart,
+      //   MultipartState_SecondPart,
+      //   MultipartState_NextParts
+      // };
+
+      // enum StreamState
+      // {
+      //   StreamState_None,
+      //   StreamState_Opened
+      // };
+
+      enum State
+      {
+        State_None,
+        State_MultipartFirstPart,
+        State_MultipartSecondPart,
+        State_MultipartNextParts,
+        State_WritingStream
+
       };
 
+      // enum StreamState
+      // {
+      //   StreamState_None,
+      //   StreamState_Opened
+      // };
+
       HttpOutput&                 output_;
       std::unique_ptr<std::string>  errorDetails_;
       bool                        logDetails_;
-      MultipartState              multipartState_;
+      // MultipartState              multipartState_;
       std::string                 multipartSubType_;
       std::string                 multipartContentType_;
       std::string                 multipartFirstPart_;
       std::map<std::string, std::string>  multipartFirstHeaders_;
-      
+      // StreamState                 streamState_;
+      State                       state_;
+
     public:
       explicit PluginHttpOutput(HttpOutput& output) :
         output_(output),
         logDetails_(false),
-        multipartState_(MultipartState_None)
+        state_(State_None)
+        // multipartState_(MultipartState_None),
+        // streamState_(StreamState_None)
       {
       }
 
       HttpOutput& GetOutput()
       {
-        if (multipartState_ == MultipartState_None)
+        // if (multipartState_ == MultipartState_None)
+        if (state_ == State_None)
         {
           return output_;
         }
         else
         {
-          // Must use "SendMultipartItem()" on multipart streams
+          // Must use "SendMultipartItem()" on multipart streams or SendStreamChunk
           throw OrthancException(ErrorCode_BadSequenceOfCalls);
         }
       }
@@ -1171,18 +1198,47 @@
       void StartMultipart(const char* subType,
                           const char* contentType)
       {
-        if (multipartState_ != MultipartState_None)
+        // if (multipartState_ != MultipartState_None)
+        if (state_ != State_None)
         {
           throw OrthancException(ErrorCode_BadSequenceOfCalls);
         }
         else
         {
-          multipartState_ = MultipartState_FirstPart;
+          // multipartState_ = MultipartState_FirstPart;
+          state_ = State_MultipartFirstPart;
           multipartSubType_ = subType;
           multipartContentType_ = contentType;
         }
       }
 
+      void StartStream(const char* contentType)
+      {
+        if (state_ != State_None)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          output_.StartStream(contentType);
+          state_ = State_WritingStream;
+        }
+      }
+
+      void SendStreamItem(const void* data,
+                          size_t size)
+      {
+        if (state_ != State_WritingStream)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          output_.SendStreamItem(data, size);
+        }
+      }
+
+
       void SendMultipartItem(const void* data,
                              size_t size,
                              const std::map<std::string, std::string>& headers)
@@ -1192,19 +1248,19 @@
           throw OrthancException(ErrorCode_NullPointer);
         }
 
-        switch (multipartState_)
+        switch (state_)
         {
-          case MultipartState_None:
+          case State_None:
             // Must call "StartMultipart()" before
             throw OrthancException(ErrorCode_BadSequenceOfCalls);
 
-          case MultipartState_FirstPart:
+          case State_MultipartFirstPart:
             multipartFirstPart_.assign(reinterpret_cast<const char*>(data), size);
             multipartFirstHeaders_ = headers;
-            multipartState_ = MultipartState_SecondPart;
+            state_ = State_MultipartSecondPart;
             break;
 
-          case MultipartState_SecondPart:
+          case State_MultipartSecondPart:
             // Start an actual stream for chunked transfer as soon as
             // there are more than 2 elements in the multipart stream
             output_.StartMultipart(multipartSubType_, multipartContentType_);
@@ -1213,10 +1269,10 @@
             multipartFirstPart_.clear();  // Release memory
 
             output_.SendMultipartItem(data, size, headers);
-            multipartState_ = MultipartState_NextParts;
+            state_ = State_MultipartNextParts;
             break;
 
-          case MultipartState_NextParts:
+          case State_MultipartNextParts:
             output_.SendMultipartItem(data, size, headers);
             break;
 
@@ -1230,21 +1286,21 @@
       {
         if (error == OrthancPluginErrorCode_Success)
         {
-          switch (multipartState_)
+          switch (state_)
           {
-            case MultipartState_None:
+            case State_None:
               assert(!output_.IsWritingMultipart());
               break;
 
-            case MultipartState_FirstPart:   // Multipart started, but no part was sent
-            case MultipartState_SecondPart:  // Multipart started, first part is pending
+            case State_MultipartFirstPart:   // Multipart started, but no part was sent
+            case State_MultipartSecondPart:  // Multipart started, first part is pending
             {
               assert(!output_.IsWritingMultipart());
               std::vector<const void*> parts;
               std::vector<size_t> sizes;
               std::vector<const std::map<std::string, std::string>*> headers;
 
-              if (multipartState_ == MultipartState_SecondPart)
+              if (state_ == State_MultipartSecondPart)
               {
                 parts.push_back(multipartFirstPart_.c_str());
                 sizes.push_back(multipartFirstPart_.size());
@@ -1256,10 +1312,14 @@
               break;
             }
 
-            case MultipartState_NextParts:
+            case State_MultipartNextParts:
               assert(output_.IsWritingMultipart());
               output_.CloseMultipart();
 
+            case State_WritingStream:
+              assert(output_.IsWritingStream());
+              output_.CloseStream();
+
             default:
               throw OrthancException(ErrorCode_InternalError);
           }
@@ -4892,6 +4952,21 @@
         ApplySendMultipartItem2(parameters);
         return true;
 
+      case _OrthancPluginService_StartStreamAnswer:
+      {
+        const _OrthancPluginStartStreamAnswer& p =
+          *reinterpret_cast<const _OrthancPluginStartStreamAnswer*>(parameters);
+        reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->StartStream(p.contentType);
+        return true;
+      }
+
+      case _OrthancPluginService_SendStreamChunk:
+      {
+        const _OrthancPluginAnswerBuffer& p =
+          *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
+        reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->SendStreamItem(p.answer, p.answerSize);
+        return true;
+      }
       case _OrthancPluginService_ReadFile:
       {
         const _OrthancPluginReadFile& p =
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jan 08 15:18:00 2025 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Jan 10 18:27:27 2025 +0100
@@ -507,6 +507,8 @@
     _OrthancPluginService_CompressAndAnswerImage = 2011,
     _OrthancPluginService_SendMultipartItem2 = 2012,
     _OrthancPluginService_SetHttpErrorDetails = 2013,
+    _OrthancPluginService_StartStreamAnswer = 2014,
+    _OrthancPluginService_SendStreamChunk = 2015,
 
     /* Access to the Orthanc database and API */
     _OrthancPluginService_GetDicomForInstance = 3000,
@@ -9568,6 +9570,66 @@
   }
 
 
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              contentType;
+  } _OrthancPluginStartStreamAnswer;
+
+  /**
+   * @brief Start an HTTP stream answer.
+   *
+   * Initiates an HTTP stream answer, as the result of a REST request.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param contentType The MIME type of the items in the stream answer.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginSendStreamChunk()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartStreamAnswer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              contentType)
+  {
+    _OrthancPluginStartStreamAnswer params;
+    params.output = output;
+    params.contentType = contentType;
+    return context->InvokeService(context, _OrthancPluginService_StartStreamAnswer, &params);
+  }
+
+
+  /**
+   * @brief Send a chunk as a part of an HTTP stream answer.
+   *
+   * This function sends a chunk as part of an HTTP stream
+   * answer that was initiated by OrthancPluginStartStreamAnswer().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginStartStreamAnswer()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendStreamChunk(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = NULL;
+    return context->InvokeService(context, _OrthancPluginService_SendStreamChunk, &params);
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif