view Framework/Oracle/WebAssemblyOracle.cpp @ 956:a7351ad54960

Made IsContextLost automatically set the flag by checking with the emscripten WebGL wrapper + added a LOT of logging messages right before throwing ErrorCode_BadSequenceOfCalls exceptions + increased the http request timeouts from 60 to 600 sec (big datasets in some recent customer use cases) + added IsContext lost through the Viewport/Context layer (to make it reachable from external API) + the same for the underlying device context (for debug)
author Benjamin Golinvaux <bgo@osimis.io>
date Wed, 21 Aug 2019 16:16:30 +0200
parents f75f6cb69c1b
children 13e078adfb94
line wrap: on
line source

/**
 * Stone of Orthanc
 * 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 Affero General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 **/


#include "WebAssemblyOracle.h"

#include "SleepOracleCommand.h"

#include <Core/OrthancException.h>
#include <Core/Toolbox.h>

#include <emscripten.h>
#include <emscripten/html5.h>
#include <emscripten/fetch.h>


namespace OrthancStone
{
  class WebAssemblyOracle::TimeoutContext
  {
  private:
    WebAssemblyOracle&                 oracle_;
    const IObserver&                   receiver_;
    std::auto_ptr<SleepOracleCommand>  command_;

  public:
    TimeoutContext(WebAssemblyOracle& oracle,
                   const IObserver& receiver,
                   IOracleCommand* command) :
      oracle_(oracle),
      receiver_(receiver)
    {
      if (command == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
      else
      {
        command_.reset(dynamic_cast<SleepOracleCommand*>(command));
      }
    }

    void EmitMessage()
    {
      SleepOracleCommand::TimeoutMessage message(*command_);
      oracle_.EmitMessage(receiver_, message);
    }

    static void Callback(void *userData)
    {
      std::auto_ptr<TimeoutContext> context(reinterpret_cast<TimeoutContext*>(userData));
      context->EmitMessage();
    }
  };
    

  class WebAssemblyOracle::Emitter : public IMessageEmitter
  {
  private:
    WebAssemblyOracle&  oracle_;

  public:
    Emitter(WebAssemblyOracle&  oracle) :
      oracle_(oracle)
    {
    }

    virtual void EmitMessage(const IObserver& receiver,
                             const IMessage& message)
    {
      oracle_.EmitMessage(receiver, message);
    }
  };


  class WebAssemblyOracle::FetchContext : public boost::noncopyable
  {
  private:
    Emitter                        emitter_;
    const IObserver&               receiver_;
    std::auto_ptr<IOracleCommand>  command_;
    std::string                    expectedContentType_;

  public:
    FetchContext(WebAssemblyOracle& oracle,
                 const IObserver& receiver,
                 IOracleCommand* command,
                 const std::string& expectedContentType) :
      emitter_(oracle),
      receiver_(receiver),
      command_(command),
      expectedContentType_(expectedContentType)
    {
      if (command == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }

    const std::string& GetExpectedContentType() const
    {
      return expectedContentType_;
    }

    void EmitMessage(const IMessage& message)
    {
      emitter_.EmitMessage(receiver_, message);
    }

    IMessageEmitter& GetEmitter()
    {
      return emitter_;
    }

    const IObserver& GetReceiver() const
    {
      return receiver_;
    }

    IOracleCommand& GetCommand() const
    {
      return *command_;
    }

    template <typename T>
    const T& GetTypedCommand() const
    {
      return dynamic_cast<T&>(*command_);
    }

    static void SuccessCallback(emscripten_fetch_t *fetch)
    {
      /**
       * Firstly, make a local copy of the fetched information, and
       * free data associated with the fetch.
       **/
      
      std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
      if (fetch->userData == NULL)
      {
        LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!!!!!!!";
      }

      std::string answer;
      if (fetch->numBytes > 0)
      {
        answer.assign(fetch->data, fetch->numBytes);
      }

      /**
       * TODO - HACK - As of emscripten-1.38.31, the fetch API does
       * not contain a way to retrieve the HTTP headers of the
       * answer. We make the assumption that the "Content-Type" header
       * of the response is the same as the "Accept" header of the
       * query. This should be fixed in future versions of emscripten.
       * https://github.com/emscripten-core/emscripten/pull/8486
       **/

      HttpHeaders headers;
      if (fetch->userData != NULL)
      {
        if (!context->GetExpectedContentType().empty())
        {
          headers["Content-Type"] = context->GetExpectedContentType();
        }
      }
      
      emscripten_fetch_close(fetch);


      /**
       * Secondly, use the retrieved data.
       **/

      try
      {
        if (context.get() == NULL)
        {
          throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
        }
        else
        {
          switch (context->GetCommand().GetType())
          {
            case IOracleCommand::Type_OrthancRestApi:
            {
              OrthancRestApiCommand::SuccessMessage message
                (context->GetTypedCommand<OrthancRestApiCommand>(), headers, answer);
              context->EmitMessage(message);
              break;
            }
            
            case IOracleCommand::Type_GetOrthancImage:
            {
              context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer
                (context->GetEmitter(), context->GetReceiver(), answer, headers);
              break;
            }
          
            case IOracleCommand::Type_GetOrthancWebViewerJpeg:
            {
              context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer
                (context->GetEmitter(), context->GetReceiver(), answer);
              break;
            }
          
            default:
              LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: "
                         << context->GetCommand().GetType();
          }
        }
      }
      catch (Orthanc::OrthancException& e)
      {
        LOG(ERROR) << "Error while processing a fetch answer in the oracle: " << e.What();
      }
    }

    static void FailureCallback(emscripten_fetch_t *fetch)
    {
      std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
      
      LOG(ERROR) << "Fetching " << fetch->url << " failed, HTTP failure status code: " << fetch->status;

      /**
       * TODO - The following code leads to an infinite recursion, at
       * least with Firefox running on incognito mode => WHY?
       **/      
      //emscripten_fetch_close(fetch); // Also free data on failure.
    }
  };
    


  class WebAssemblyOracle::FetchCommand : public boost::noncopyable
  {
  private:
    WebAssemblyOracle&             oracle_;
    const IObserver&               receiver_;
    std::auto_ptr<IOracleCommand>  command_;
    Orthanc::HttpMethod            method_;
    std::string                    uri_;
    std::string                    body_;
    HttpHeaders                    headers_;
    unsigned int                   timeout_;
    std::string                    expectedContentType_;

  public:
    FetchCommand(WebAssemblyOracle& oracle,
                 const IObserver& receiver,
                 IOracleCommand* command) :
      oracle_(oracle),
      receiver_(receiver),
      command_(command),
      method_(Orthanc::HttpMethod_Get),
      timeout_(0)
    {
      if (command == NULL)
      {
        throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
      }
    }

    void SetMethod(Orthanc::HttpMethod method)
    {
      method_ = method;
    }

    void SetUri(const std::string& uri)
    {
      uri_ = oracle_.orthancRoot_ + uri;
    }

    void SetBody(std::string& body /* will be swapped */)
    {
      body_.swap(body);
    }

    void SetHttpHeaders(const HttpHeaders& headers)
    {
      headers_ = headers;
    }

    void SetTimeout(unsigned int timeout)
    {
      timeout_ = timeout;
    }

    void Execute()
    {
      if (command_.get() == NULL)
      {
        // Cannot call Execute() twice
        LOG(ERROR) << "WebAssemblyOracle::Execute(): (command_.get() == NULL)";
        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
      }

      emscripten_fetch_attr_t attr;
      emscripten_fetch_attr_init(&attr);

      const char* method;
      
      switch (method_)
      {
        case Orthanc::HttpMethod_Get:
          method = "GET";
          break;

        case Orthanc::HttpMethod_Post:
          method = "POST";
          break;

        case Orthanc::HttpMethod_Delete:
          method = "DELETE";
          break;

        case Orthanc::HttpMethod_Put:
          method = "PUT";
          break;

        default:
          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
      }

      strcpy(attr.requestMethod, method);

      attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
      attr.onsuccess = FetchContext::SuccessCallback;
      attr.onerror = FetchContext::FailureCallback;
      attr.timeoutMSecs = timeout_ * 1000;

      std::vector<const char*> headers;
      headers.reserve(2 * headers_.size() + 1);

      std::string expectedContentType;
        
      for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
      {
        std::string key;
        Orthanc::Toolbox::ToLowerCase(key, it->first);
          
        if (key == "accept")
        {
          expectedContentType = it->second;
        }

        if (key != "accept-encoding")  // Web browsers forbid the modification of this HTTP header
        {
          headers.push_back(it->first.c_str());
          headers.push_back(it->second.c_str());
        }
      }
        
      headers.push_back(NULL);  // Termination of the array of HTTP headers

      attr.requestHeaders = &headers[0];

      char* requestData = NULL;
      if (!body_.empty())
        requestData = reinterpret_cast<char*>(malloc(body_.size()));
        
      try 
      {
        if (!body_.empty())
        {
          memcpy(requestData, &(body_[0]), body_.size());
          attr.requestDataSize = body_.size();
          attr.requestData = requestData;
        }
        attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType);

        // Must be the last call to prevent memory leak on error
        emscripten_fetch(&attr, uri_.c_str());
      }        
      catch(...)
      {
        if(requestData != NULL)
          free(requestData);
        throw;
      }
    }
  };
    
  void WebAssemblyOracle::Execute(const IObserver& receiver,
                                  OrthancRestApiCommand* command)
  {
    try
    {
      //LOG(TRACE) << "*********** WebAssemblyOracle::Execute.";
      //LOG(TRACE) << "WebAssemblyOracle::Execute | command = " << command;
      FetchCommand fetch(*this, receiver, command);

      fetch.SetMethod(command->GetMethod());
      fetch.SetUri(command->GetUri());
      fetch.SetHttpHeaders(command->GetHttpHeaders());
      fetch.SetTimeout(command->GetTimeout());

      if (command->GetMethod() == Orthanc::HttpMethod_Post ||
        command->GetMethod() == Orthanc::HttpMethod_Put)
      {
        std::string body;
        command->SwapBody(body);
        fetch.SetBody(body);
      }

      fetch.Execute();
      //LOG(TRACE) << "*********** successful end of WebAssemblyOracle::Execute.";
    }
    catch (const Orthanc::OrthancException& e)
    {
      if (e.HasDetails())
      {
        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What() << " Details: " << e.GetDetails();
      }
      else
      {
        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What();
      }
      //LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
      throw;
    }
    catch (const std::exception& e)
    {
      LOG(ERROR) << "std::exception in WebAssemblyOracle::Execute: " << e.what();
//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
      throw;
    }
    catch (...)
    {
      LOG(ERROR) << "Unknown exception in WebAssemblyOracle::Execute";
//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
      throw;
    }
  }
    
    
  void WebAssemblyOracle::Execute(const IObserver& receiver,
                                  GetOrthancImageCommand* command)
  {
    FetchCommand fetch(*this, receiver, command);

    fetch.SetUri(command->GetUri());
    fetch.SetHttpHeaders(command->GetHttpHeaders());
    fetch.SetTimeout(command->GetTimeout());
      
    fetch.Execute();
  }
    
    
  void WebAssemblyOracle::Execute(const IObserver& receiver,
                                  GetOrthancWebViewerJpegCommand* command)
  {
    FetchCommand fetch(*this, receiver, command);

    fetch.SetUri(command->GetUri());
    fetch.SetHttpHeaders(command->GetHttpHeaders());
    fetch.SetTimeout(command->GetTimeout());
      
    fetch.Execute();
  }



  void WebAssemblyOracle::Schedule(const IObserver& receiver,
                                   IOracleCommand* command)
  {
    std::auto_ptr<IOracleCommand> protection(command);

    if (command == NULL)
    {
      throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
    }

    switch (command->GetType())
    {
      case IOracleCommand::Type_OrthancRestApi:
        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
        //{
        //  const IObserver* pReceiver = &receiver;
        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | pReceiver is " << pReceiver;
        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | command = " << command;
        //  OrthancRestApiCommand* rac = dynamic_cast<OrthancRestApiCommand*>(protection.get());
        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | typed command = " << rac;
        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
        //}
        //// END OF BLOCK TO REMOVE
        Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
        break;
        
      case IOracleCommand::Type_GetOrthancImage:
        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
        //{
        //  GetOrthancImageCommand* rac = dynamic_cast<GetOrthancImageCommand*>(protection.get());
        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
        //}
        //// END OF BLOCK TO REMOVE
        Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
        break;

      case IOracleCommand::Type_GetOrthancWebViewerJpeg:
        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
        //{
        //  GetOrthancWebViewerJpegCommand* rac = dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.get());
        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
        //}
        //// END OF BLOCK TO REMOVE
        Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
        break;          
            
      case IOracleCommand::Type_Sleep:
      {
        unsigned int timeoutMS = dynamic_cast<SleepOracleCommand*>(command)->GetDelay();
        emscripten_set_timeout(TimeoutContext::Callback, timeoutMS,
                               new TimeoutContext(*this, receiver, protection.release()));
        break;
      }
            
      default:
        LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType();
    }
  }
}