# HG changeset patch # User Sebastien Jodogne # Date 1790599520 -7200 # Node ID 62b1a4463d11b76bd18670ba3f110fe9373aa114 # Parent cb959d9a539af1ae85e8ba64266a5f399e04b72f reorganization diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake --- a/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake Sun Sep 27 12:16:39 2026 +0200 +++ b/OrthancStone/Resources/CMake/OrthancStoneConfiguration.cmake Mon Sep 28 14:45:20 2026 +0200 @@ -220,8 +220,8 @@ if (NOT ORTHANC_SANDBOXED AND ENABLE_THREADS AND ENABLE_WEB_CLIENT) list(APPEND ORTHANC_STONE_SOURCES ${ORTHANC_STONE_ROOT}/Loaders/GenericLoadersContext.cpp - ${ORTHANC_STONE_ROOT}/Oracle/GenericOracleRunner.cpp - ${ORTHANC_STONE_ROOT}/Oracle/ThreadedOracle.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Native/GenericOracleRunner.cpp + ${ORTHANC_STONE_ROOT}/Platforms/Native/ThreadedOracle.cpp ${ORTHANC_STONE_ROOT}/Platforms/Native/NativeEnvironment.cpp ${ORTHANC_STONE_ROOT}/Platforms/Native/RunnableThread.cpp ) diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Loaders/GenericLoadersContext.cpp --- a/OrthancStone/Sources/Loaders/GenericLoadersContext.cpp Sun Sep 27 12:16:39 2026 +0200 +++ b/OrthancStone/Sources/Loaders/GenericLoadersContext.cpp Mon Sep 28 14:45:20 2026 +0200 @@ -23,7 +23,7 @@ #include "GenericLoadersContext.h" -#include "../Oracle/ThreadedOracle.h" +#include "../Platforms/Native/ThreadedOracle.h" #include "../StoneApplication.h" #include diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Oracle/GenericOracleRunner.cpp --- a/OrthancStone/Sources/Oracle/GenericOracleRunner.cpp Sun Sep 27 12:16:39 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,529 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * . - **/ - - -#include "GenericOracleRunner.h" - -#if !defined(ORTHANC_ENABLE_DCMTK) -# error The macro ORTHANC_ENABLE_DCMTK must be defined -#endif - -#include "GetOrthancImageCommand.h" -#include "GetOrthancWebViewerJpegCommand.h" -#include "HttpCommand.h" -#include "OracleCommandExceptionMessage.h" -#include "OrthancRestApiCommand.h" -#include "ParseDicomFromFileCommand.h" -#include "ParseDicomFromWadoCommand.h" -#include "ReadFileCommand.h" - -#if ORTHANC_ENABLE_DCMTK == 1 -# include "ParseDicomSuccessMessage.h" -# include -# include -static unsigned int BUCKET_DICOMDIR = 0; -static unsigned int BUCKET_SOP = 1; -#endif - -#include -#include -#include -#include -#include -#include - -#include - - - -namespace OrthancStone -{ - static void CopyHttpHeaders(Orthanc::HttpClient& client, - const Orthanc::HttpClient::HttpHeaders& headers) - { - for (Orthanc::HttpClient::HttpHeaders::const_iterator - it = headers.begin(); it != headers.end(); ++it ) - { - client.AddHeader(it->first, it->second); - } - } - - - static void DecodeAnswer(std::string& answer, - const Orthanc::HttpClient::HttpHeaders& headers) - { - Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None; - - for (Orthanc::HttpClient::HttpHeaders::const_iterator it = headers.begin(); - it != headers.end(); ++it) - { - std::string s; - Orthanc::Toolbox::ToLowerCase(s, it->first); - - if (s == "content-encoding") - { - if (it->second == "gzip") - { - contentEncoding = Orthanc::HttpCompression_Gzip; - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, - "Unsupported HTTP Content-Encoding: " + it->second); - } - - break; - } - } - - if (contentEncoding == Orthanc::HttpCompression_Gzip) - { - std::string compressed; - answer.swap(compressed); - - Orthanc::GzipCompressor compressor; - compressor.Uncompress(answer, compressed.c_str(), compressed.size()); - - LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() - << " to " << answer.size() << " bytes"; - } - } - - - static void RunHttpCommand(std::string& answer, - Orthanc::HttpClient::HttpHeaders& answerHeaders, - const HttpCommand& command) - { - Orthanc::HttpClient client; - client.SetUrl(command.GetUrl()); - client.SetMethod(command.GetMethod()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - if (command.HasCredentials()) - { - client.SetCredentials(command.GetUsername().c_str(), command.GetPassword().c_str()); - } - - if (command.GetMethod() == Orthanc::HttpMethod_Post || - command.GetMethod() == Orthanc::HttpMethod_Put) - { - client.SetExternalBody(command.GetBody()); - } - - client.ApplyAndThrowException(answer, answerHeaders); - client.ClearBody(); - - DecodeAnswer(answer, answerHeaders); - } - - - static void RunInternal(IOracleCallback& callback, - const HttpCommand& command) - { - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - RunHttpCommand(answer, answerHeaders, command); - - std::unique_ptr message(new HttpCommand::SuccessMessage(command, answerHeaders)); - message->SwapAnswer(answer); - - callback.NotifySuccess(message.release()); - } - - - static void RunOrthancRestApiCommand(std::string& answer, - Orthanc::HttpClient::HttpHeaders& answerHeaders, - const Orthanc::WebServiceParameters& orthanc, - const OrthancRestApiCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetRedirectionFollowed(false); - client.SetMethod(command.GetMethod()); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - if (command.GetMethod() == Orthanc::HttpMethod_Post || - command.GetMethod() == Orthanc::HttpMethod_Put) - { - client.SetExternalBody(command.GetBody()); - } - - client.ApplyAndThrowException(answer, answerHeaders); - client.ClearBody(); - DecodeAnswer(answer, answerHeaders); - } - - - static void RunInternal(IOracleCallback& callback, - const Orthanc::WebServiceParameters& orthanc, - const OrthancRestApiCommand& command) - { - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - RunOrthancRestApiCommand(answer, answerHeaders, orthanc, command); - - std::unique_ptr message(new OrthancRestApiCommand::SuccessMessage(command, answerHeaders)); - message->SwapAnswer(answer); - - callback.NotifySuccess(message.release()); - } - - - static void RunInternal(IOracleCallback& callback, - const Orthanc::WebServiceParameters& orthanc, - const GetOrthancImageCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetRedirectionFollowed(false); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - command.ProcessHttpAnswer(callback, answer, answerHeaders); - } - - - static void RunInternal(IOracleCallback& callback, - const Orthanc::WebServiceParameters& orthanc, - const GetOrthancWebViewerJpegCommand& command) - { - Orthanc::HttpClient client(orthanc, command.GetUri()); - client.SetRedirectionFollowed(false); - client.SetTimeout(command.GetTimeout()); - - CopyHttpHeaders(client, command.GetHttpHeaders()); - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - client.ApplyAndThrowException(answer, answerHeaders); - - DecodeAnswer(answer, answerHeaders); - - command.ProcessHttpAnswer(callback, answer); - } - - - static std::string GetPath(const std::string& root, - const std::string& file) - { - boost::filesystem::path a(root); - boost::filesystem::path b(file); - - boost::filesystem::path c; - if (b.is_absolute()) - { - c = b; - } - else - { - c = a / b; - } - - return c.string(); - } - - - static void RunInternal(IOracleCallback& callback, - const std::string& root, - const ReadFileCommand& command) - { - std::string path = GetPath(root, command.GetPath()); - LOG(TRACE) << "Oracle reading file: " << path; - - std::string content; - Orthanc::SystemToolbox::ReadFile(content, path, true /* log */); - - std::unique_ptr message(new ReadFileCommand::SuccessMessage(command)); - message->SwapContent(content); - callback.NotifySuccess(message.release()); - } - - -#if ORTHANC_ENABLE_DCMTK == 1 - static Orthanc::ParsedDicomFile* ParseDicom(uint64_t& fileSize, /* OUT */ - const std::string& path, - bool isPixelData) - { - if (!Orthanc::SystemToolbox::IsRegularFile(path)) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); - } - - LOG(TRACE) << "Parsing DICOM file, " << (isPixelData ? "with" : "without") - << " pixel data: " << path; - - boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); - - fileSize = Orthanc::SystemToolbox::GetFileSize(path); - - // Check for 32bit systems - if (fileSize != static_cast(static_cast(fileSize))) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); - } - - DcmFileFormat dicom; - bool ok; - - if (isPixelData) - { - ok = dicom.loadFile(path.c_str()).good(); - } - else - { -#if DCMTK_VERSION_NUMBER >= 362 - /** - * NB : We could stop at (0x3007, 0x0000) instead of - * DCM_PixelData as the Stone framework does not use further - * tags (cf. the Orthanc::DICOM_TAG_* constants), but we still - * use "PixelData" as this does not change the runtime much, and - * as it is more explicit. - **/ - static const DcmTagKey STOP = DCM_PixelData; - //static const DcmTagKey STOP(0x3007, 0x0000); - - ok = dicom.loadFileUntilTag(path.c_str(), EXS_Unknown, EGL_noChange, - DCM_MaxReadLength, ERM_autoDetect, STOP).good(); -#else - // The primitive "loadFileUntilTag" was introduced in DCMTK 3.6.2 - ok = dicom.loadFile(path.c_str()).good(); -#endif - } - - if (ok) - { - std::unique_ptr result(new Orthanc::ParsedDicomFile(dicom)); - - boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); - LOG(TRACE) << path << ": parsed in " << (end-start).total_milliseconds() << " ms"; - - return result.release(); - } - else - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, - "Cannot parse file: " + path); - } - } - - - static void RunInternal(IOracleCallback& callback, - boost::shared_ptr cache, - const std::string& root, - const ParseDicomFromFileCommand& command) - { - const std::string path = GetPath(root, command.GetPath()); - -#if 0 - // TODO Refactoring - Reactivate the cache!!! - - if (cache) - { - ParsedDicomCache::Reader reader(*cache, BUCKET_DICOMDIR, path); - if (reader.IsValid() && - (!command.IsPixelDataIncluded() || - reader.HasPixelData())) - { - // Reuse the DICOM file from the cache - callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), reader.GetDicom(), - reader.GetFileSize(), reader.HasPixelData())); - return; - } - } -#endif - - uint64_t fileSize; - boost::shared_ptr parsed(ParseDicom(fileSize, path, command.IsPixelDataIncluded())); - - if (fileSize != static_cast(fileSize)) - { - // Cannot load such a large file on 32-bit architecture - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); - } - - callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), parsed, - static_cast(fileSize), command.IsPixelDataIncluded())); - -#if 0 - // TODO Refactoring - Reactivate the cache!!! - if (cache) - { - // Store it into the cache for future use - - // Invalidate to overwrite DICOM instance that would already - // be stored without pixel data - cache->Invalidate(BUCKET_DICOMDIR, path); - - cache->Acquire(BUCKET_DICOMDIR, path, parsed.release(), - static_cast(fileSize), command.IsPixelDataIncluded()); - } -#endif - } - - - static void RunInternal(IOracleCallback& callback, - boost::shared_ptr cache, - const Orthanc::WebServiceParameters& orthanc, - const ParseDicomFromWadoCommand& command) - { -#if 0 - // TODO Refactoring - Reactivate the cache!!! - if (cache) - { - ParsedDicomCache::Reader reader(*cache, BUCKET_SOP, command.GetSopInstanceUid()); - if (reader.IsValid() && - reader.HasPixelData()) - { - // Reuse the DICOM file from the cache - callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), reader.GetDicom(), - reader.GetFileSize(), reader.HasPixelData())); - return; - } - } -#endif - - std::string answer; - Orthanc::HttpClient::HttpHeaders answerHeaders; - - switch (command.GetRestCommand().GetType()) - { - case IOracleCommand::Type_Http: - RunHttpCommand(answer, answerHeaders, dynamic_cast(command.GetRestCommand())); - break; - - case IOracleCommand::Type_OrthancRestApi: - RunOrthancRestApiCommand(answer, answerHeaders, orthanc, - dynamic_cast(command.GetRestCommand())); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - - size_t fileSize; - boost::shared_ptr parsed(ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, answerHeaders)); - - callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), parsed, fileSize, - true /* pixel data always is included in WADO-RS */)); - -#if 0 - // TODO Refactoring - Reactivate the cache!!! - if (cache) - { - // Store it into the cache for future use - cache->Acquire(BUCKET_SOP, command.GetSopInstanceUid(), parsed.release(), fileSize, true); - } -#endif - } -#endif - - - void GenericOracleRunner::Run(IOracleCallback& callback) - { - Orthanc::ErrorCode error = Orthanc::ErrorCode_Success; - - try - { - switch (callback.GetCommand().GetType()) - { - case IOracleCommand::Type_Sleep: - throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, - "Sleep command cannot be executed by the runner"); - - case IOracleCommand::Type_Http: - RunInternal(callback, dynamic_cast(callback.GetCommand())); - break; - - case IOracleCommand::Type_OrthancRestApi: - RunInternal(callback, configuration_.GetRemoteOrthancParameters(), - dynamic_cast(callback.GetCommand())); - break; - - case IOracleCommand::Type_GetOrthancImage: - RunInternal(callback, configuration_.GetRemoteOrthancParameters(), - dynamic_cast(callback.GetCommand())); - break; - - case IOracleCommand::Type_GetOrthancWebViewerJpeg: - RunInternal(callback, configuration_.GetRemoteOrthancParameters(), - dynamic_cast(callback.GetCommand())); - break; - - case IOracleCommand::Type_ReadFile: - RunInternal(callback, configuration_.GetRootDirectory(), - dynamic_cast(callback.GetCommand())); - break; - - case IOracleCommand::Type_ParseDicomFromFile: - case IOracleCommand::Type_ParseDicomFromWado: -#if ORTHANC_ENABLE_DCMTK == 1 - switch (callback.GetCommand().GetType()) - { - case IOracleCommand::Type_ParseDicomFromFile: - RunInternal(callback, dicomCache_, configuration_.GetRootDirectory(), - dynamic_cast(callback.GetCommand())); - break; - - case IOracleCommand::Type_ParseDicomFromWado: - RunInternal(callback, dicomCache_, configuration_.GetRemoteOrthancParameters(), - dynamic_cast(callback.GetCommand())); - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); - } - break; -#else - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, - "DCMTK must be enabled to parse DICOM files"); -#endif - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); - } - } - catch (Orthanc::OrthancException& e) - { - LOG(ERROR) << "Exception within the oracle: " << e.What(); - error = e.GetErrorCode(); - } - catch (...) - { - LOG(ERROR) << "Threaded exception within the oracle"; - error = Orthanc::ErrorCode_InternalError; - } - - if (error != Orthanc::ErrorCode_Success) - { - callback.NotifyError(Orthanc::OrthancException(error)); - } - } -} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Oracle/GenericOracleRunner.h --- a/OrthancStone/Sources/Oracle/GenericOracleRunner.h Sun Sep 27 12:16:39 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,69 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * . - **/ - - -#pragma once - -#include // To have the macros properly defined - -#if !defined(ORTHANC_ENABLE_DCMTK) -# error The macro ORTHANC_ENABLE_DCMTK must be defined -#endif - -#if ORTHANC_ENABLE_DCMTK == 1 -# include "../Toolbox/ParsedDicomCache.h" -#endif - -#include "../StoneApplication.h" -#include "IOracleCommand.h" -#include "OracleCallback.h" - -#include // For ORTHANC_OVERRIDE -#include - -namespace OrthancStone -{ - class GenericOracleRunner : public boost::noncopyable - { - private: - StoneApplication::Configuration configuration_; - -#if ORTHANC_ENABLE_DCMTK == 1 - boost::shared_ptr dicomCache_; -#endif - - public: - GenericOracleRunner(const StoneApplication::Configuration& configuration) : - configuration_(configuration) - { - } - -#if ORTHANC_ENABLE_DCMTK == 1 - void SetDicomCache(boost::shared_ptr cache) - { - dicomCache_ = cache; - } -#endif - - void Run(IOracleCallback& callback); - }; -} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Oracle/ThreadedOracle.cpp --- a/OrthancStone/Sources/Oracle/ThreadedOracle.cpp Sun Sep 27 12:16:39 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,232 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * . - **/ - - -#include "ThreadedOracle.h" - -#include "GenericOracleRunner.h" -#include "OracleCallback.h" -#include "SleepOracleCommand.h" - -#include -#include - -namespace OrthancStone -{ - class ThreadedOracle::GenericRunnable : public Orthanc::IRunnable - { - private: - StoneApplication::Configuration configuration_; - std::unique_ptr runner_; - std::unique_ptr callback_; - - public: - GenericRunnable(const StoneApplication::Configuration& configuration, - GenericOracleRunner* runner /* takes ownership */, - IOracleCallback* callback /* takes ownership */) : - configuration_(configuration), - runner_(runner), - callback_(callback) - { - if (runner == NULL || - callback == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - } - - virtual void Run() ORTHANC_OVERRIDE - { - runner_->Run(*callback_); - } - }; - - - class ThreadedOracle::SleepRunnable : public Orthanc::IRunnable - { - private: - class Item : public boost::noncopyable - { - private: - std::unique_ptr callback_; - boost::posix_time::ptime expiration_; - - public: - Item(IOracleCallback* callback, - unsigned int delay) : - callback_(callback) - { - if (callback == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - expiration_ = (boost::posix_time::microsec_clock::local_time() + - boost::posix_time::milliseconds(delay)); - } - - const boost::posix_time::ptime& GetExpirationTime() const - { - return expiration_; - } - - IOracleCallback& GetCallback() - { - return *callback_; - } - }; - - typedef std::list Content; - - boost::mutex mutex_; - Content content_; - - public: - ~SleepRunnable() - { - for (Content::iterator it = content_.begin(); it != content_.end(); ++it) - { - if (*it != NULL) - { - delete *it; - } - } - } - - - void Add(IOracleCallback* callback, - unsigned int delay) - { - boost::mutex::scoped_lock lock(mutex_); - content_.push_back(new Item(callback, delay)); - } - - - // Awakes expired sleeps - virtual void Run() ORTHANC_OVERRIDE - { - boost::mutex::scoped_lock lock(mutex_); - - const boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time(); - - Content stillSleeping; - - for (Content::iterator it = content_.begin(); it != content_.end(); ++it) - { - if (*it != NULL && - (*it)->GetExpirationTime() <= now) - { - const SleepOracleCommand& command = dynamic_cast((*it)->GetCallback().GetCommand()); // TODO Refactoring - Remove this - (*it)->GetCallback().NotifySuccess(new SleepOracleCommand::TimeoutMessage(command)); - delete *it; - *it = NULL; - } - else - { - stillSleeping.push_back(*it); - } - } - - // Compact the still-sleeping commands - content_ = stillSleeping; - } - }; - - - ThreadedOracle::ThreadedOracle(const StoneApplication::Configuration& configuration) : - configuration_(configuration), - sleepingThread_(new SleepRunnable, configuration.GetWorkersTimeResolution()) - { - threadPool_.SetThreadsCount(configuration.GetOracleThreadsCount()); - threadPool_.SetDequeueTimeout(configuration.GetWorkersTimeResolution()); - - if (configuration.GetDicomCacheSize() == 0) - { - LOG(WARNING) << "The DICOM cache is disabled"; - } - else - { - LOG(INFO) << "The DICOM cache size is set to " << configuration.GetDicomCacheSize() << " bytes"; - dicomCache_.reset(new ParsedDicomCache(configuration.GetDicomCacheSize())); - } - } - - - void ThreadedOracle::Start() - { - sleepingThread_.Start(); - threadPool_.Start(); - } - - - void ThreadedOracle::Stop() - { - threadPool_.Stop(); - sleepingThread_.Stop(); - } - - - void ThreadedOracle::SubmitInternal(IOracleCallback* callback /* takes ownership */) - { - std::unique_ptr protection(callback); - - if (callback == NULL) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - if (protection->GetCommand().GetType() == IOracleCommand::Type_Sleep) - { - const unsigned int delay = dynamic_cast(protection->GetCommand()).GetDelay(); - SleepRunnable& runnable = dynamic_cast(sleepingThread_.GetRunnable()); - runnable.Add(protection.release(), delay); - } - else - { - std::unique_ptr runner(new GenericOracleRunner(configuration_)); - -#if ORTHANC_ENABLE_DCMTK == 1 - if (dicomCache_) - { - runner->SetDicomCache(dicomCache_); - } -#endif - - threadPool_.Submit(new GenericRunnable(configuration_, runner.release(), protection.release())); - } - } - - - void ThreadedOracle::Submit(IEnvironment& environment, - const boost::shared_ptr& client, - IOracleCommand* command /* takes ownership */) - { - std::unique_ptr protection(command); - - if (!client) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); - } - - SubmitInternal(new OracleCallback(environment, client, protection.release())); - } -} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Oracle/ThreadedOracle.h --- a/OrthancStone/Sources/Oracle/ThreadedOracle.h Sun Sep 27 12:16:39 2026 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -/** - * Stone of Orthanc - * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics - * Department, University Hospital of Liege, Belgium - * Copyright (C) 2017-2023 Osimis S.A., Belgium - * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium - * - * This program is free software: you can redistribute it and/or - * modify it under the terms of the GNU Lesser 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program. If not, see - * . - **/ - - -#pragma once - -#include // To have the macros properly defined -#include // ORTHANC_OVERRIDE - -#if !defined(ORTHANC_ENABLE_THREADS) -# error The macro ORTHANC_ENABLE_THREADS must be defined -#endif - -#if !defined(ORTHANC_ENABLE_DCMTK) -# error The macro ORTHANC_ENABLE_DCMTK must be defined -#endif - -#if ORTHANC_ENABLE_THREADS != 1 -# error This file can only compiled for native targets -#endif - -#if ORTHANC_ENABLE_DCMTK == 1 -# include "../Toolbox/ParsedDicomCache.h" -#endif - -#include "../Messages/IMessageEmitter.h" -#include "../Platforms/Native/RunnableThread.h" -#include "../StoneApplication.h" -#include "IOracle.h" -#include "OracleCallback.h" - -#include - - -namespace OrthancStone -{ - class ThreadedOracle : public IOracle - { - private: - class GenericRunnable; - class SleepRunnable; - - StoneApplication::Configuration configuration_; - RunnableThread sleepingThread_; - Orthanc::ThreadPool threadPool_; - -#if ORTHANC_ENABLE_DCMTK == 1 - boost::shared_ptr dicomCache_; -#endif - - void SubmitInternal(IOracleCallback* callback /* takes ownership */); - - public: - ThreadedOracle(const StoneApplication::Configuration& configuration); - - void Start(); - - void Stop(); - - virtual void Submit(IEnvironment& environment, - const boost::shared_ptr& client, - IOracleCommand* command /* takes ownership */) ORTHANC_OVERRIDE; - }; -} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Platforms/Native/GenericOracleRunner.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Platforms/Native/GenericOracleRunner.cpp Mon Sep 28 14:45:20 2026 +0200 @@ -0,0 +1,529 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "GenericOracleRunner.h" + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#include "../../Oracle/GetOrthancImageCommand.h" +#include "../../Oracle/GetOrthancWebViewerJpegCommand.h" +#include "../../Oracle/HttpCommand.h" +#include "../../Oracle/OracleCommandExceptionMessage.h" +#include "../../Oracle/OrthancRestApiCommand.h" +#include "../../Oracle/ParseDicomFromFileCommand.h" +#include "../../Oracle/ParseDicomFromWadoCommand.h" +#include "../../Oracle/ReadFileCommand.h" + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../../Oracle/ParseDicomSuccessMessage.h" +# include +# include +static unsigned int BUCKET_DICOMDIR = 0; +static unsigned int BUCKET_SOP = 1; +#endif + +#include +#include +#include +#include +#include +#include + +#include + + + +namespace OrthancStone +{ + static void CopyHttpHeaders(Orthanc::HttpClient& client, + const Orthanc::HttpClient::HttpHeaders& headers) + { + for (Orthanc::HttpClient::HttpHeaders::const_iterator + it = headers.begin(); it != headers.end(); ++it ) + { + client.AddHeader(it->first, it->second); + } + } + + + static void DecodeAnswer(std::string& answer, + const Orthanc::HttpClient::HttpHeaders& headers) + { + Orthanc::HttpCompression contentEncoding = Orthanc::HttpCompression_None; + + for (Orthanc::HttpClient::HttpHeaders::const_iterator it = headers.begin(); + it != headers.end(); ++it) + { + std::string s; + Orthanc::Toolbox::ToLowerCase(s, it->first); + + if (s == "content-encoding") + { + if (it->second == "gzip") + { + contentEncoding = Orthanc::HttpCompression_Gzip; + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, + "Unsupported HTTP Content-Encoding: " + it->second); + } + + break; + } + } + + if (contentEncoding == Orthanc::HttpCompression_Gzip) + { + std::string compressed; + answer.swap(compressed); + + Orthanc::GzipCompressor compressor; + compressor.Uncompress(answer, compressed.c_str(), compressed.size()); + + LOG(INFO) << "Uncompressing gzip Encoding: from " << compressed.size() + << " to " << answer.size() << " bytes"; + } + } + + + static void RunHttpCommand(std::string& answer, + Orthanc::HttpClient::HttpHeaders& answerHeaders, + const HttpCommand& command) + { + Orthanc::HttpClient client; + client.SetUrl(command.GetUrl()); + client.SetMethod(command.GetMethod()); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + if (command.HasCredentials()) + { + client.SetCredentials(command.GetUsername().c_str(), command.GetPassword().c_str()); + } + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetExternalBody(command.GetBody()); + } + + client.ApplyAndThrowException(answer, answerHeaders); + client.ClearBody(); + + DecodeAnswer(answer, answerHeaders); + } + + + static void RunInternal(IOracleCallback& callback, + const HttpCommand& command) + { + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + RunHttpCommand(answer, answerHeaders, command); + + std::unique_ptr message(new HttpCommand::SuccessMessage(command, answerHeaders)); + message->SwapAnswer(answer); + + callback.NotifySuccess(message.release()); + } + + + static void RunOrthancRestApiCommand(std::string& answer, + Orthanc::HttpClient::HttpHeaders& answerHeaders, + const Orthanc::WebServiceParameters& orthanc, + const OrthancRestApiCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetMethod(command.GetMethod()); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + if (command.GetMethod() == Orthanc::HttpMethod_Post || + command.GetMethod() == Orthanc::HttpMethod_Put) + { + client.SetExternalBody(command.GetBody()); + } + + client.ApplyAndThrowException(answer, answerHeaders); + client.ClearBody(); + DecodeAnswer(answer, answerHeaders); + } + + + static void RunInternal(IOracleCallback& callback, + const Orthanc::WebServiceParameters& orthanc, + const OrthancRestApiCommand& command) + { + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + RunOrthancRestApiCommand(answer, answerHeaders, orthanc, command); + + std::unique_ptr message(new OrthancRestApiCommand::SuccessMessage(command, answerHeaders)); + message->SwapAnswer(answer); + + callback.NotifySuccess(message.release()); + } + + + static void RunInternal(IOracleCallback& callback, + const Orthanc::WebServiceParameters& orthanc, + const GetOrthancImageCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + client.ApplyAndThrowException(answer, answerHeaders); + + DecodeAnswer(answer, answerHeaders); + + command.ProcessHttpAnswer(callback, answer, answerHeaders); + } + + + static void RunInternal(IOracleCallback& callback, + const Orthanc::WebServiceParameters& orthanc, + const GetOrthancWebViewerJpegCommand& command) + { + Orthanc::HttpClient client(orthanc, command.GetUri()); + client.SetRedirectionFollowed(false); + client.SetTimeout(command.GetTimeout()); + + CopyHttpHeaders(client, command.GetHttpHeaders()); + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + client.ApplyAndThrowException(answer, answerHeaders); + + DecodeAnswer(answer, answerHeaders); + + command.ProcessHttpAnswer(callback, answer); + } + + + static std::string GetPath(const std::string& root, + const std::string& file) + { + boost::filesystem::path a(root); + boost::filesystem::path b(file); + + boost::filesystem::path c; + if (b.is_absolute()) + { + c = b; + } + else + { + c = a / b; + } + + return c.string(); + } + + + static void RunInternal(IOracleCallback& callback, + const std::string& root, + const ReadFileCommand& command) + { + std::string path = GetPath(root, command.GetPath()); + LOG(TRACE) << "Oracle reading file: " << path; + + std::string content; + Orthanc::SystemToolbox::ReadFile(content, path, true /* log */); + + std::unique_ptr message(new ReadFileCommand::SuccessMessage(command)); + message->SwapContent(content); + callback.NotifySuccess(message.release()); + } + + +#if ORTHANC_ENABLE_DCMTK == 1 + static Orthanc::ParsedDicomFile* ParseDicom(uint64_t& fileSize, /* OUT */ + const std::string& path, + bool isPixelData) + { + if (!Orthanc::SystemToolbox::IsRegularFile(path)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile); + } + + LOG(TRACE) << "Parsing DICOM file, " << (isPixelData ? "with" : "without") + << " pixel data: " << path; + + boost::posix_time::ptime start = boost::posix_time::microsec_clock::local_time(); + + fileSize = Orthanc::SystemToolbox::GetFileSize(path); + + // Check for 32bit systems + if (fileSize != static_cast(static_cast(fileSize))) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + DcmFileFormat dicom; + bool ok; + + if (isPixelData) + { + ok = dicom.loadFile(path.c_str()).good(); + } + else + { +#if DCMTK_VERSION_NUMBER >= 362 + /** + * NB : We could stop at (0x3007, 0x0000) instead of + * DCM_PixelData as the Stone framework does not use further + * tags (cf. the Orthanc::DICOM_TAG_* constants), but we still + * use "PixelData" as this does not change the runtime much, and + * as it is more explicit. + **/ + static const DcmTagKey STOP = DCM_PixelData; + //static const DcmTagKey STOP(0x3007, 0x0000); + + ok = dicom.loadFileUntilTag(path.c_str(), EXS_Unknown, EGL_noChange, + DCM_MaxReadLength, ERM_autoDetect, STOP).good(); +#else + // The primitive "loadFileUntilTag" was introduced in DCMTK 3.6.2 + ok = dicom.loadFile(path.c_str()).good(); +#endif + } + + if (ok) + { + std::unique_ptr result(new Orthanc::ParsedDicomFile(dicom)); + + boost::posix_time::ptime end = boost::posix_time::microsec_clock::local_time(); + LOG(TRACE) << path << ": parsed in " << (end-start).total_milliseconds() << " ms"; + + return result.release(); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, + "Cannot parse file: " + path); + } + } + + + static void RunInternal(IOracleCallback& callback, + boost::shared_ptr cache, + const std::string& root, + const ParseDicomFromFileCommand& command) + { + const std::string path = GetPath(root, command.GetPath()); + +#if 0 + // TODO Refactoring - Reactivate the cache!!! + + if (cache) + { + ParsedDicomCache::Reader reader(*cache, BUCKET_DICOMDIR, path); + if (reader.IsValid() && + (!command.IsPixelDataIncluded() || + reader.HasPixelData())) + { + // Reuse the DICOM file from the cache + callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), reader.GetDicom(), + reader.GetFileSize(), reader.HasPixelData())); + return; + } + } +#endif + + uint64_t fileSize; + boost::shared_ptr parsed(ParseDicom(fileSize, path, command.IsPixelDataIncluded())); + + if (fileSize != static_cast(fileSize)) + { + // Cannot load such a large file on 32-bit architecture + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory); + } + + callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), parsed, + static_cast(fileSize), command.IsPixelDataIncluded())); + +#if 0 + // TODO Refactoring - Reactivate the cache!!! + if (cache) + { + // Store it into the cache for future use + + // Invalidate to overwrite DICOM instance that would already + // be stored without pixel data + cache->Invalidate(BUCKET_DICOMDIR, path); + + cache->Acquire(BUCKET_DICOMDIR, path, parsed.release(), + static_cast(fileSize), command.IsPixelDataIncluded()); + } +#endif + } + + + static void RunInternal(IOracleCallback& callback, + boost::shared_ptr cache, + const Orthanc::WebServiceParameters& orthanc, + const ParseDicomFromWadoCommand& command) + { +#if 0 + // TODO Refactoring - Reactivate the cache!!! + if (cache) + { + ParsedDicomCache::Reader reader(*cache, BUCKET_SOP, command.GetSopInstanceUid()); + if (reader.IsValid() && + reader.HasPixelData()) + { + // Reuse the DICOM file from the cache + callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), reader.GetDicom(), + reader.GetFileSize(), reader.HasPixelData())); + return; + } + } +#endif + + std::string answer; + Orthanc::HttpClient::HttpHeaders answerHeaders; + + switch (command.GetRestCommand().GetType()) + { + case IOracleCommand::Type_Http: + RunHttpCommand(answer, answerHeaders, dynamic_cast(command.GetRestCommand())); + break; + + case IOracleCommand::Type_OrthancRestApi: + RunOrthancRestApiCommand(answer, answerHeaders, orthanc, + dynamic_cast(command.GetRestCommand())); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + + size_t fileSize; + boost::shared_ptr parsed(ParseDicomSuccessMessage::ParseWadoAnswer(fileSize, answer, answerHeaders)); + + callback.NotifySuccess(new ParseDicomSuccessMessage(command, command.GetSource(), parsed, fileSize, + true /* pixel data always is included in WADO-RS */)); + +#if 0 + // TODO Refactoring - Reactivate the cache!!! + if (cache) + { + // Store it into the cache for future use + cache->Acquire(BUCKET_SOP, command.GetSopInstanceUid(), parsed.release(), fileSize, true); + } +#endif + } +#endif + + + void GenericOracleRunner::Run(IOracleCallback& callback) + { + Orthanc::ErrorCode error = Orthanc::ErrorCode_Success; + + try + { + switch (callback.GetCommand().GetType()) + { + case IOracleCommand::Type_Sleep: + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadParameterType, + "Sleep command cannot be executed by the runner"); + + case IOracleCommand::Type_Http: + RunInternal(callback, dynamic_cast(callback.GetCommand())); + break; + + case IOracleCommand::Type_OrthancRestApi: + RunInternal(callback, configuration_.GetRemoteOrthancParameters(), + dynamic_cast(callback.GetCommand())); + break; + + case IOracleCommand::Type_GetOrthancImage: + RunInternal(callback, configuration_.GetRemoteOrthancParameters(), + dynamic_cast(callback.GetCommand())); + break; + + case IOracleCommand::Type_GetOrthancWebViewerJpeg: + RunInternal(callback, configuration_.GetRemoteOrthancParameters(), + dynamic_cast(callback.GetCommand())); + break; + + case IOracleCommand::Type_ReadFile: + RunInternal(callback, configuration_.GetRootDirectory(), + dynamic_cast(callback.GetCommand())); + break; + + case IOracleCommand::Type_ParseDicomFromFile: + case IOracleCommand::Type_ParseDicomFromWado: +#if ORTHANC_ENABLE_DCMTK == 1 + switch (callback.GetCommand().GetType()) + { + case IOracleCommand::Type_ParseDicomFromFile: + RunInternal(callback, dicomCache_, configuration_.GetRootDirectory(), + dynamic_cast(callback.GetCommand())); + break; + + case IOracleCommand::Type_ParseDicomFromWado: + RunInternal(callback, dicomCache_, configuration_.GetRemoteOrthancParameters(), + dynamic_cast(callback.GetCommand())); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + break; +#else + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "DCMTK must be enabled to parse DICOM files"); +#endif + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } + } + catch (Orthanc::OrthancException& e) + { + LOG(ERROR) << "Exception within the oracle: " << e.What(); + error = e.GetErrorCode(); + } + catch (...) + { + LOG(ERROR) << "Threaded exception within the oracle"; + error = Orthanc::ErrorCode_InternalError; + } + + if (error != Orthanc::ErrorCode_Success) + { + callback.NotifyError(Orthanc::OrthancException(error)); + } + } +} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Platforms/Native/GenericOracleRunner.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Platforms/Native/GenericOracleRunner.h Mon Sep 28 14:45:20 2026 +0200 @@ -0,0 +1,69 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include // To have the macros properly defined + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../../Toolbox/ParsedDicomCache.h" +#endif + +#include "../../Oracle/IOracleCommand.h" +#include "../../Oracle/OracleCallback.h" +#include "../../StoneApplication.h" + +#include // For ORTHANC_OVERRIDE +#include + +namespace OrthancStone +{ + class GenericOracleRunner : public boost::noncopyable + { + private: + StoneApplication::Configuration configuration_; + +#if ORTHANC_ENABLE_DCMTK == 1 + boost::shared_ptr dicomCache_; +#endif + + public: + GenericOracleRunner(const StoneApplication::Configuration& configuration) : + configuration_(configuration) + { + } + +#if ORTHANC_ENABLE_DCMTK == 1 + void SetDicomCache(boost::shared_ptr cache) + { + dicomCache_ = cache; + } +#endif + + void Run(IOracleCallback& callback); + }; +} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Platforms/Native/ThreadedOracle.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Platforms/Native/ThreadedOracle.cpp Mon Sep 28 14:45:20 2026 +0200 @@ -0,0 +1,232 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#include "ThreadedOracle.h" + +#include "../../Oracle/OracleCallback.h" +#include "../../Oracle/SleepOracleCommand.h" +#include "GenericOracleRunner.h" + +#include +#include + +namespace OrthancStone +{ + class ThreadedOracle::GenericRunnable : public Orthanc::IRunnable + { + private: + StoneApplication::Configuration configuration_; + std::unique_ptr runner_; + std::unique_ptr callback_; + + public: + GenericRunnable(const StoneApplication::Configuration& configuration, + GenericOracleRunner* runner /* takes ownership */, + IOracleCallback* callback /* takes ownership */) : + configuration_(configuration), + runner_(runner), + callback_(callback) + { + if (runner == NULL || + callback == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + } + + virtual void Run() ORTHANC_OVERRIDE + { + runner_->Run(*callback_); + } + }; + + + class ThreadedOracle::SleepRunnable : public Orthanc::IRunnable + { + private: + class Item : public boost::noncopyable + { + private: + std::unique_ptr callback_; + boost::posix_time::ptime expiration_; + + public: + Item(IOracleCallback* callback, + unsigned int delay) : + callback_(callback) + { + if (callback == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + expiration_ = (boost::posix_time::microsec_clock::local_time() + + boost::posix_time::milliseconds(delay)); + } + + const boost::posix_time::ptime& GetExpirationTime() const + { + return expiration_; + } + + IOracleCallback& GetCallback() + { + return *callback_; + } + }; + + typedef std::list Content; + + boost::mutex mutex_; + Content content_; + + public: + ~SleepRunnable() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + if (*it != NULL) + { + delete *it; + } + } + } + + + void Add(IOracleCallback* callback, + unsigned int delay) + { + boost::mutex::scoped_lock lock(mutex_); + content_.push_back(new Item(callback, delay)); + } + + + // Awakes expired sleeps + virtual void Run() ORTHANC_OVERRIDE + { + boost::mutex::scoped_lock lock(mutex_); + + const boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time(); + + Content stillSleeping; + + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + if (*it != NULL && + (*it)->GetExpirationTime() <= now) + { + const SleepOracleCommand& command = dynamic_cast((*it)->GetCallback().GetCommand()); // TODO Refactoring - Remove this + (*it)->GetCallback().NotifySuccess(new SleepOracleCommand::TimeoutMessage(command)); + delete *it; + *it = NULL; + } + else + { + stillSleeping.push_back(*it); + } + } + + // Compact the still-sleeping commands + content_ = stillSleeping; + } + }; + + + ThreadedOracle::ThreadedOracle(const StoneApplication::Configuration& configuration) : + configuration_(configuration), + sleepingThread_(new SleepRunnable, configuration.GetWorkersTimeResolution()) + { + threadPool_.SetThreadsCount(configuration.GetOracleThreadsCount()); + threadPool_.SetDequeueTimeout(configuration.GetWorkersTimeResolution()); + + if (configuration.GetDicomCacheSize() == 0) + { + LOG(WARNING) << "The DICOM cache is disabled"; + } + else + { + LOG(INFO) << "The DICOM cache size is set to " << configuration.GetDicomCacheSize() << " bytes"; + dicomCache_.reset(new ParsedDicomCache(configuration.GetDicomCacheSize())); + } + } + + + void ThreadedOracle::Start() + { + sleepingThread_.Start(); + threadPool_.Start(); + } + + + void ThreadedOracle::Stop() + { + threadPool_.Stop(); + sleepingThread_.Stop(); + } + + + void ThreadedOracle::SubmitInternal(IOracleCallback* callback /* takes ownership */) + { + std::unique_ptr protection(callback); + + if (callback == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + if (protection->GetCommand().GetType() == IOracleCommand::Type_Sleep) + { + const unsigned int delay = dynamic_cast(protection->GetCommand()).GetDelay(); + SleepRunnable& runnable = dynamic_cast(sleepingThread_.GetRunnable()); + runnable.Add(protection.release(), delay); + } + else + { + std::unique_ptr runner(new GenericOracleRunner(configuration_)); + +#if ORTHANC_ENABLE_DCMTK == 1 + if (dicomCache_) + { + runner->SetDicomCache(dicomCache_); + } +#endif + + threadPool_.Submit(new GenericRunnable(configuration_, runner.release(), protection.release())); + } + } + + + void ThreadedOracle::Submit(IEnvironment& environment, + const boost::shared_ptr& client, + IOracleCommand* command /* takes ownership */) + { + std::unique_ptr protection(command); + + if (!client) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + + SubmitInternal(new OracleCallback(environment, client, protection.release())); + } +} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/Platforms/Native/ThreadedOracle.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancStone/Sources/Platforms/Native/ThreadedOracle.h Mon Sep 28 14:45:20 2026 +0200 @@ -0,0 +1,83 @@ +/** + * Stone of Orthanc + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see + * . + **/ + + +#pragma once + +#include // To have the macros properly defined +#include // ORTHANC_OVERRIDE + +#if !defined(ORTHANC_ENABLE_THREADS) +# error The macro ORTHANC_ENABLE_THREADS must be defined +#endif + +#if !defined(ORTHANC_ENABLE_DCMTK) +# error The macro ORTHANC_ENABLE_DCMTK must be defined +#endif + +#if ORTHANC_ENABLE_THREADS != 1 +# error This file can only compiled for native targets +#endif + +#if ORTHANC_ENABLE_DCMTK == 1 +# include "../../Toolbox/ParsedDicomCache.h" +#endif + +#include "../../Messages/IMessageEmitter.h" +#include "../../Oracle/IOracle.h" +#include "../../Oracle/OracleCallback.h" +#include "../../StoneApplication.h" +#include "RunnableThread.h" + +#include + + +namespace OrthancStone +{ + class ThreadedOracle : public IOracle + { + private: + class GenericRunnable; + class SleepRunnable; + + StoneApplication::Configuration configuration_; + RunnableThread sleepingThread_; + Orthanc::ThreadPool threadPool_; + +#if ORTHANC_ENABLE_DCMTK == 1 + boost::shared_ptr dicomCache_; +#endif + + void SubmitInternal(IOracleCallback* callback /* takes ownership */); + + public: + ThreadedOracle(const StoneApplication::Configuration& configuration); + + void Start(); + + void Stop(); + + virtual void Submit(IEnvironment& environment, + const boost::shared_ptr& client, + IOracleCommand* command /* takes ownership */) ORTHANC_OVERRIDE; + }; +} diff -r cb959d9a539a -r 62b1a4463d11 OrthancStone/Sources/StoneApplication.cpp --- a/OrthancStone/Sources/StoneApplication.cpp Sun Sep 27 12:16:39 2026 +0200 +++ b/OrthancStone/Sources/StoneApplication.cpp Mon Sep 28 14:45:20 2026 +0200 @@ -33,7 +33,7 @@ # include "Platforms/WebAssembly/WebAssemblyOracle.h" #elif ORTHANC_STONE_TARGET_PLATFORM_NATIVE == 1 # include "Platforms/Native/NativeEnvironment.h" -# include "Oracle/ThreadedOracle.h" +# include "Platforms/Native/ThreadedOracle.h" #else # error Support your platform here #endif