# HG changeset patch # User Sebastien Jodogne # Date 1786516437 -7200 # Node ID 45817b3c16643bb16c7cb45ed54ad0975f1a58e9 # Parent 2a54e6538924ac1d61ad091e7c9983a0945821f7# Parent 923bec010d541bae5ccf4a09d06bcef3c5cfa8a2 integration mainline->streaming diff -r 2a54e6538924 -r 45817b3c1664 NEWS --- a/NEWS Mon Aug 10 14:59:05 2026 +0200 +++ b/NEWS Wed Aug 12 08:33:57 2026 +0200 @@ -20,6 +20,7 @@ - "SequentialDicomReaderWindowCapacity" And new metrics: - TODO list +* Multiple paths to configuration files can now be provided, instead of a single file or a single folder * Orthanc will now refuse to start if you have "AuthenticationEnabled" and "RemoteAccessAllowed" set to true and have not defined any users in "RegisteredUsers". * New values for "OverwriteInstances" configuration. In previous versions, this configuration @@ -41,7 +42,7 @@ - Some line of log now contain additional contextual informations like the job id they relate to. A new "--logs-no-context" command line option can be used to get back to the previous behavior to keep backward compatibility and reduce the lines length. - Another "--logs-threadnames-in-context" command line option can be used to add more information in the + Another "--logs-thread-names-in-context" command line option can be used to add more information in the contexts wrt the thread that has requested a service. This is mainly for developers. * C-Find SCP: - When Orthanc was requested a sequence in a C-Find query, Orthanc was actually not returning @@ -53,6 +54,9 @@ * Fix mismatches between the configuration default values and the documentation: - "HttpTimeout" was documented as 60 while its default value is 0 (no timeout). - "Name" was documented as "MyOrthanc" while its default value is "ORTHANC". +* Orthanc no longer accepts DICOM association from modalities whose AET contains invalid characters. +* Orthanc now ignores leading/trailing spaces in AET. + REST API -------- diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Resources/CMake/BoostConfiguration.cmake --- a/OrthancFramework/Resources/CMake/BoostConfiguration.cmake Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake Wed Aug 12 08:33:57 2026 +0200 @@ -38,6 +38,10 @@ list(APPEND ORTHANC_BOOST_COMPONENTS locale) endif() + if (ENABLE_BOOST_PROGRAM_OPTIONS) + list(APPEND ORTHANC_BOOST_COMPONENTS program_options) + endif() + list(APPEND ORTHANC_BOOST_COMPONENTS filesystem thread date_time regex iostreams) find_package(Boost COMPONENTS ${ORTHANC_BOOST_COMPONENTS}) @@ -441,6 +445,27 @@ endif() + ## + ## Configuration of boost::program_options (new in Orthanc 1.13.0) + ## + + if (ENABLE_BOOST_PROGRAM_OPTIONS) + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/program_options/src/cmdline.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/config_file.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/convert.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/options_description.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/parsers.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/positional_options.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/split.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/utf8_codecvt_facet.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/value_semantic.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/variables_map.cpp + # ${BOOST_SOURCES_DIR}/libs/program_options/src/winmain.cpp + ) + endif() + + source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*) endif() diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake --- a/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Resources/CMake/BoostConfigurationStatic-1.69.0.cmake Wed Aug 12 08:33:57 2026 +0200 @@ -360,6 +360,27 @@ endif() + ## + ## Configuration of boost::program_options (new in Orthanc 1.13.0) + ## + + if (ENABLE_BOOST_PROGRAM_OPTIONS) + list(APPEND BOOST_SOURCES + ${BOOST_SOURCES_DIR}/libs/program_options/src/cmdline.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/config_file.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/convert.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/options_description.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/parsers.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/positional_options.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/split.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/utf8_codecvt_facet.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/value_semantic.cpp + ${BOOST_SOURCES_DIR}/libs/program_options/src/variables_map.cpp + # ${BOOST_SOURCES_DIR}/libs/program_options/src/winmain.cpp + ) + endif() + + source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*) endif() diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake --- a/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake Wed Aug 12 08:33:57 2026 +0200 @@ -137,6 +137,7 @@ set(ENABLE_DCMTK_NETWORKING OFF CACHE INTERNAL "Enable DICOM networking in DCMTK") set(ENABLE_DCMTK_TRANSCODING OFF CACHE INTERNAL "Enable DICOM transcoding in DCMTK") set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL") +set(ENABLE_BOOST_PROGRAM_OPTIONS OFF CACHE BOOL "Enable support for the boost::program_options library") # New in Orthanc 1.13.0 set(ORTHANC_SANDBOXED OFF CACHE INTERNAL "Whether Orthanc runs inside a sandboxed environment (such as Google NaCl or WebAssembly)") diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp --- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -328,6 +328,15 @@ calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); } + if (!Toolbox::IsValidAet(remoteAet)) + { + LOG(ERROR) << "Association Received from an invalid AET (sanitized: " << Toolbox::NormalizeAet(remoteAet) << ")" + << " on IP " << remoteIp << ", rejecting the association"; + return NULL; + } + + remoteAet = Toolbox::NormalizeAet(remoteAet); // removes leading/trailing spaces that are not significant according to the standard + CLOG(INFO, DICOM) << "Association Received from AET " << remoteAet << " on IP " << remoteIp; diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Sources/HttpServer/HttpServer.cpp --- a/OrthancFramework/Sources/HttpServer/HttpServer.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -1280,7 +1280,10 @@ struct mg_connection *connection, const struct mg_request_info *request) { - server.UpdateCurrentThreadName(); + // Since we don't control the thread creation ourselves, we set and clear + // the thread name everytime otherwise, the thread name will persist even + // after a call to /tools/reset and Orthanc would stop because the same thread name is used multiple times + Logging::ScopedCurrentThreadNameSetter setter(server.GetCurrentThreadName()); std::unique_ptr counter(server.CreateAvailableHttpThreadsDecounter()); @@ -1825,8 +1828,11 @@ void HttpServer::Start() { - // reset thread counter used to generate HTTP thread names. - threadCounter_ = 0; + { + // reset thread counter used to generate HTTP thread names. + boost::unique_lock lock(threadCounterMutex_); + threadCounter_ = 0; + } #if ORTHANC_ENABLE_MONGOOSE == 1 CLOG(INFO, HTTP) << "Starting embedded Web server using Mongoose"; @@ -2467,13 +2473,25 @@ } - void HttpServer::UpdateCurrentThreadName() + std::string HttpServer::GetCurrentThreadName() { // threads are created in CivetWeb -> assign them a name the first time they are used - if (!Logging::HasCurrentThreadName()) + const boost::thread::id threadId = boost::this_thread::get_id(); + + boost::upgrade_lock readerLock(threadCounterMutex_); // reader lock to check if the threadId has already been registered + + if (threadNames_.find(threadId) == threadNames_.end()) { - boost::mutex::scoped_lock lock(threadCounterMutex_); - Logging::SetCurrentThreadName(std::string("HTTP-") + boost::lexical_cast(threadCounter_++)); + boost::upgrade_to_unique_lock writerLock(readerLock); + + std::string thisThreadName = std::string("HTTP-") + boost::lexical_cast(threadCounter_++); + threadNames_[threadId] = thisThreadName; + + return thisThreadName; + } + else + { + return threadNames_[threadId]; } } diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Sources/HttpServer/HttpServer.h --- a/OrthancFramework/Sources/HttpServer/HttpServer.h Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Sources/HttpServer/HttpServer.h Wed Aug 12 08:33:57 2026 +0200 @@ -54,6 +54,7 @@ #include #include #include +#include namespace Orthanc @@ -118,8 +119,9 @@ unsigned int requestTimeout_; // In seconds std::unique_ptr availableHttpThreadsMetrics_; // New in Orthanc 1.12.9 - boost::mutex threadCounterMutex_; // New in Orthanc 1.12.9 - uint16_t threadCounter_; // Introduced as a global, static variable in Orthanc 1.12.2 + boost::shared_mutex threadCounterMutex_; // protects threadCounter_ and threadNames_; + uint16_t threadCounter_; + std::map threadNames_; // New in Orthanc 1.12.11 bool hasMaxBodySize_; @@ -247,7 +249,7 @@ // Can return NULL if SetMetricsRegistry() was not call beforehand MetricsRegistry::AvailableResourcesDecounter* CreateAvailableHttpThreadsDecounter(); - void UpdateCurrentThreadName(); + std::string GetCurrentThreadName(); void SetMaxBodySize(uint64_t size); diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Sources/SystemToolbox.cpp --- a/OrthancFramework/Sources/SystemToolbox.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Sources/SystemToolbox.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -227,8 +227,8 @@ { if (!IsRegularFile(path)) { - throw OrthancException(ErrorCode_RegularFileExpected, - "The path does not point to a regular file: " + PathToUtf8(path), log); + throw OrthancException(ErrorCode_RegularFileExpected, + "Not a regular file or file not found: " + PathToUtf8(path), log); } try @@ -258,11 +258,13 @@ } catch (boost::filesystem::filesystem_error&) { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } catch (...) // To catch "std::system_error&" in C++11 { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } } @@ -273,8 +275,8 @@ { if (!IsRegularFile(path)) { - throw OrthancException(ErrorCode_RegularFileExpected, - "The path does not point to a regular file: " + PathToUtf8(path)); + throw OrthancException(ErrorCode_RegularFileExpected, + "Not a regular file or file not found: " + PathToUtf8(path)); } try @@ -283,7 +285,8 @@ f.open(path, std::ifstream::in | std::ifstream::binary); if (!f.good()) { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } bool full = true; @@ -314,11 +317,13 @@ } catch (boost::filesystem::filesystem_error&) { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } catch (...) // To catch "std::system_error&" in C++11 { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } } @@ -414,7 +419,8 @@ } else { - throw OrthancException(ErrorCode_RegularFileExpected); + throw OrthancException(ErrorCode_RegularFileExpected, + "Not a regular file or file not found: " + PathToUtf8(path)); } } } @@ -428,11 +434,13 @@ } catch (boost::filesystem::filesystem_error&) { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } catch (...) // To catch "std::system_error&" in C++11 { - throw OrthancException(ErrorCode_InexistentFile); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } } @@ -1068,16 +1076,16 @@ if (!IsRegularFile(path)) { - throw OrthancException(ErrorCode_RegularFileExpected, - "The path does not point to a regular file: " + SystemToolbox::PathToUtf8(path)); + throw OrthancException(ErrorCode_RegularFileExpected, + "Not a regular file or file not found: " + PathToUtf8(path)); } boost::filesystem::ifstream f; f.open(path, std::ifstream::in | std::ifstream::binary); if (!f.good()) { - throw OrthancException(ErrorCode_InexistentFile, - "File not found: " + SystemToolbox::PathToUtf8(path)); + throw OrthancException(ErrorCode_InexistentFile, + "File not found: " + PathToUtf8(path)); } uint64_t fileSize = static_cast(GetStreamSize(f)); diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Sources/Toolbox.cpp --- a/OrthancFramework/Sources/Toolbox.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Sources/Toolbox.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -3022,6 +3022,60 @@ } + bool Toolbox::IsValidAet(const std::string& aet) + { + if (aet.size() == 0) + { + return false; + } + + const uint8_t* p = reinterpret_cast(aet.c_str()); + size_t spacesCount = 0; + + for (size_t i = 0; i < aet.size(); ++i, ++p) + { + if (iscntrl(*p) || *p == '\\') // formaly, only LF, FF, CR and ESC and \ are invalid but let's refuse any control character to be extra safe. + { + return false; + } + else if (*p == ' ') + { + ++spacesCount; + } + } + + if (spacesCount == aet.size()) // an AET made of only spaces is invalid + { + return false; + } + + return true; + } + + + // replaces any invalid char by * such that the AET is safe to log + // + removes leading and trailing spaces that must be ignored + std::string Toolbox::NormalizeAet(const std::string& aet) + { + std::string normalizedAet; + + for (size_t i = 0; i < aet.size(); ++i) + { + if (iscntrl(aet[i]) || + aet[i] == '\\') + { + normalizedAet.push_back('*'); + } + else + { + normalizedAet.push_back(aet[i]); + } + } + + return StripSpaces(normalizedAet); // remove any leading/trailing spaces (all leading \t\n\r have already been sanitized before) + } + + std::string Toolbox::NormalizePath(const std::string& utf8, bool allowUtf8, bool allowSlashes) diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/Sources/Toolbox.h --- a/OrthancFramework/Sources/Toolbox.h Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/Sources/Toolbox.h Wed Aug 12 08:33:57 2026 +0200 @@ -411,6 +411,10 @@ static bool IsValidUtf8(const std::string& s); + static bool IsValidAet(const std::string& aet); + + static std::string NormalizeAet(const std::string& aet); + static std::string NormalizePath(const std::string& utf8, bool allowUtf8, bool allowSlashes); diff -r 2a54e6538924 -r 45817b3c1664 OrthancFramework/UnitTestsSources/ToolboxTests.cpp --- a/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -461,6 +461,28 @@ ASSERT_FALSE(Orthanc::Toolbox::IsValidUtf8("\xF0\x28\x8C\x28")); } +TEST(Toolbox, IsValidAet) +{ + ASSERT_TRUE(Orthanc::Toolbox::IsValidAet("ABC_DEF")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet(" ")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet(" ")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("NEW_LINE\n")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("CAR_RET\r")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet("\\SLASH")); + ASSERT_FALSE(Orthanc::Toolbox::IsValidAet(" \x01CNTRL")); +} + +TEST(Toolbox, NormalizeAet) +{ + ASSERT_EQ("ABC_DEF", Orthanc::Toolbox::NormalizeAet(" ABC_DEF ")); + ASSERT_EQ("NEW_LINE*", Orthanc::Toolbox::NormalizeAet("NEW_LINE\n")); + ASSERT_EQ("WITH MIDDLE SPACE", Orthanc::Toolbox::NormalizeAet("WITH MIDDLE SPACE")); + ASSERT_EQ("CAR_RET*", Orthanc::Toolbox::NormalizeAet("CAR_RET\r")); + ASSERT_EQ("SLASH*", Orthanc::Toolbox::NormalizeAet("SLASH\\")); + ASSERT_EQ("CNTRL*", Orthanc::Toolbox::NormalizeAet("CNTRL\x01")); +} + TEST(Toolbox, BoundMemorySizeToCurrentArchitecture) { if (sizeof(void*) == 4) diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/CMakeLists.txt Wed Aug 12 08:33:57 2026 +0200 @@ -32,6 +32,7 @@ include(${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake) # Enable all the optional components of the Orthanc framework +set(ENABLE_BOOST_PROGRAM_OPTIONS ON) set(ENABLE_CRYPTO_OPTIONS ON) set(ENABLE_DCMTK ON) set(ENABLE_DCMTK_NETWORKING ON) @@ -260,6 +261,7 @@ set(ORTHANC_EMBEDDED_FILES CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/Configuration.json + ADVANCED_CONFIGURATION_SAMPLE ${CMAKE_SOURCE_DIR}/Resources/AdvancedConfiguration.json DICOM_CONFORMANCE_STATEMENT ${CMAKE_SOURCE_DIR}/Resources/DicomConformanceStatement.txt FONT_UBUNTU_MONO_BOLD_16 ${CMAKE_SOURCE_DIR}/Resources/Fonts/UbuntuMonoBold-16.json LUA_TOOLBOX ${CMAKE_SOURCE_DIR}/Resources/Toolbox.lua diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Plugins/Samples/Common/OrthancPluginException.h --- a/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h Wed Aug 12 08:33:57 2026 +0200 @@ -48,11 +48,16 @@ errorCode, #errorCode " triggered from " __ORTHANC_FILE__ ":" \ PLUGIN_ORTHANC_EXCEPTION_STRINGIFY_LINE(__LINE__)) +# define PLUGIN_THROW_WITH_FILE_AND_LINE_INFO_HELPER(errorCode, errorCodeStr) \ + throw ::Orthanc::OrthancException( \ + errorCode, "Plugin error code " + errorCodeStr + " triggered from " __ORTHANC_FILE__ ":" \ + PLUGIN_ORTHANC_EXCEPTION_STRINGIFY_LINE(__LINE__)) + # define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code) \ - PLUGIN_THROW_WITH_FILE_AND_LINE_INFO(static_cast(code)) + PLUGIN_THROW_WITH_FILE_AND_LINE_INFO_HELPER(static_cast(code), boost::lexical_cast(code)) # define ORTHANC_PLUGINS_THROW_EXCEPTION(code) \ - PLUGIN_THROW_WITH_FILE_AND_LINE_INFO(ORTHANC_PLUGINS_GET_ERROR_CODE(code)) + PLUGIN_THROW_WITH_FILE_AND_LINE_INFO_HELPER(ORTHANC_PLUGINS_GET_ERROR_CODE(code), boost::lexical_cast(ORTHANC_PLUGINS_GET_ERROR_CODE(code))) #else // the PluginException does not accept a detail argument # define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code) \ diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Resources/AdvancedConfiguration.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Resources/AdvancedConfiguration.json Wed Aug 12 08:33:57 2026 +0200 @@ -0,0 +1,9 @@ +{ + /** + * Advanced configuration options of Orthanc + * + * This file gathers advanced options that were part of the main + * configuration file until Orthanc 1.12.11. + **/ + +} diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Sources/OrthancConfiguration.cpp --- a/OrthancServer/Sources/OrthancConfiguration.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -48,45 +48,70 @@ static const char* const JOBS_ENGINE_THREADS_COUNT = "JobsEngineThreadsCount"; static const char* const DICOM_LOSSY_TRANSCODING_QUALITY = "DicomLossyTranscodingQuality"; -static Json::Value defaultConfiguration; - namespace Orthanc { - static void AddFileToConfiguration(Json::Value& target, - const boost::filesystem::path& path) + static void ReadConfigurationFromString(Json::Value& target, + const std::string& content) { std::map env; SystemToolbox::GetEnvironmentVariables(env); - - LOG(WARNING) << "Reading the configuration from: " << SystemToolbox::PathToUtf8(path); - Json::Value config; - - { - std::string content; - SystemToolbox::ReadFile(content, path); + std::string substituted = Toolbox::SubstituteVariables(content, env); - content = Toolbox::SubstituteVariables(content, env); - - Json::Value tmp; - if (!Toolbox::ReadJson(tmp, content) || - tmp.type() != Json::objectValue) - { - throw OrthancException(ErrorCode_BadJson, - "The configuration file does not follow the JSON syntax: " + SystemToolbox::PathToUtf8(path)); - } - - Toolbox::CopyJsonWithoutComments(config, tmp); + Json::Value tmp; + if (!Toolbox::ReadJson(tmp, substituted) || + tmp.type() != Json::objectValue) + { + throw OrthancException(ErrorCode_BadJson, "The configuration file does not follow the JSON syntax"); } - if (target.size() == 0) + Toolbox::CopyJsonWithoutComments(target, tmp); + } + + + static void ReadConfigurationFromFile(Json::Value& target, + const boost::filesystem::path& path) + { + LOG(WARNING) << "Reading the configuration file: " << SystemToolbox::PathToUtf8(path); + + std::string content; + SystemToolbox::ReadFile(content, path); + + ReadConfigurationFromString(target, content); + } + + + static void ReadDefaultConfiguration(Json::Value& target) + { +#if ORTHANC_STANDALONE == 1 + std::string content; + GetFileResource(content, ServerResources::CONFIGURATION_SAMPLE); + + ReadConfigurationFromString(target, content); +#else + // In a non-standalone build, we use the + // "Resources/Configuration.json" from the Orthanc source code + + boost::filesystem::path p = ORTHANC_PATH; + p /= "Resources"; + p /= "Configuration.json"; + + ReadConfigurationFromFile(target, p); +#endif + } + + + static void MergeConfigurations(Json::Value& target, + const Json::Value& source) + { + if (target.type() != Json::objectValue || + source.type() != Json::objectValue) { - target = config; + throw OrthancException(ErrorCode_InternalError); } else { - // Merge the newly-added file with the previous content of "target" - Json::Value::Members members = config.getMemberNames(); + Json::Value::Members members = source.getMemberNames(); for (Json::Value::ArrayIndex i = 0; i < members.size(); i++) { if (target.isMember(members[i])) @@ -97,24 +122,21 @@ } else { - target[members[i]] = config[members[i]]; + target[members[i]] = source[members[i]]; } } } } - - static void ScanFolderForConfiguration(Json::Value& target, - const boost::filesystem::path& folder) - { - using namespace boost::filesystem; - LOG(WARNING) << "Scanning folder \"" << Orthanc::SystemToolbox::PathToUtf8(folder) << "\" for configuration files"; + static void ReadConfigurationsFromFolder(Json::Value& target, + const boost::filesystem::path& folder) + { + LOG(WARNING) << "Scanning folder for configuration files: " << Orthanc::SystemToolbox::PathToUtf8(folder); - directory_iterator end_it; // default construction yields past-the-end - for (directory_iterator it(folder); - it != end_it; - ++it) + boost::filesystem::directory_iterator end_it; // default construction yields past-the-end + + for (boost::filesystem::directory_iterator it(folder); it != end_it; ++it) { if (!is_directory(it->status())) { @@ -123,70 +145,34 @@ if (extension == ".json") { - AddFileToConfiguration(target, it->path()); + Json::Value config; + ReadConfigurationFromFile(config, it->path()); + MergeConfigurations(target, config); } } } } - - static void ReadConfiguration(Json::Value& target, - const boost::filesystem::path &configurationFile) + + static void ReadConfiguration(Json::Value& target, + const std::list& configurationPaths) { - // lazy loading of the default configuration - if (defaultConfiguration.empty()) - { - std::string defaultConfigurationContent; - GetFileResource(defaultConfigurationContent, ServerResources::CONFIGURATION_SAMPLE); - - Json::Value tmp; - if (!Toolbox::ReadJson(tmp, defaultConfigurationContent) || - tmp.type() != Json::objectValue) - { - throw OrthancException(ErrorCode_InternalError, "The default configuration file does not follow the JSON syntax !!"); - } - - Toolbox::CopyJsonWithoutComments(defaultConfiguration, tmp); - } - target = Json::objectValue; - if (!configurationFile.empty()) + for (std::list::const_iterator it = configurationPaths.begin(); + it != configurationPaths.end(); ++it) { - if (!boost::filesystem::exists(configurationFile)) + if (boost::filesystem::is_directory(*it)) { - throw OrthancException(ErrorCode_InexistentFile, - "Inexistent path to configuration: " + - SystemToolbox::PathToUtf8(configurationFile)); - } - - if (boost::filesystem::is_directory(configurationFile)) - { - ScanFolderForConfiguration(target, configurationFile); + ReadConfigurationsFromFolder(target, *it); } else { - AddFileToConfiguration(target, configurationFile); + Json::Value config; + ReadConfigurationFromFile(config, *it); + MergeConfigurations(target, config); } } - else - { -#if ORTHANC_STANDALONE == 1 - // No default path for the standalone configuration - LOG(WARNING) << "Using the default Orthanc configuration"; - return; - -#else - // In a non-standalone build, we use the - // "Resources/Configuration.json" from the Orthanc source code - - boost::filesystem::path p = ORTHANC_PATH; - p /= "Resources"; - p /= "Configuration.json"; - - AddFileToConfiguration(target, p); -#endif - } } @@ -205,6 +191,13 @@ } + OrthancConfiguration::OrthancConfiguration() : + serverIndex_(NULL) + { + ReadDefaultConfiguration(defaultConfiguration_); + } + + void OrthancConfiguration::LoadModalitiesFromJson(const Json::Value& source) { modalities_.clear(); @@ -283,9 +276,9 @@ else { // Modalities are stored in the configuration files - if (json_.isMember(DICOM_MODALITIES)) + if (userConfiguration_.isMember(DICOM_MODALITIES)) { - LoadModalitiesFromJson(json_[DICOM_MODALITIES]); + LoadModalitiesFromJson(userConfiguration_[DICOM_MODALITIES]); } else { @@ -299,9 +292,9 @@ // default values jobsEngineThreadsCount_["ResourceModification"] = 1; - if (json_.isMember(JOBS_ENGINE_THREADS_COUNT)) + if (userConfiguration_.isMember(JOBS_ENGINE_THREADS_COUNT)) { - const Json::Value& source = json_[JOBS_ENGINE_THREADS_COUNT]; + const Json::Value& source = userConfiguration_[JOBS_ENGINE_THREADS_COUNT]; if (source.type() != Json::objectValue) { throw OrthancException(ErrorCode_BadFileFormat, @@ -343,6 +336,28 @@ return workersThread; } + void OrthancConfiguration::LoadOrthancAET() + { + std::string dicomAet = GetStringParameter(ORTHANC_CONFIG_DICOM_AET); + + if (!Toolbox::IsValidAet(dicomAet)) + { + throw OrthancException(ErrorCode_BadFileFormat, std::string("The ") + ORTHANC_CONFIG_DICOM_AET + " contains characters that are not valid for an AET"); + } + + orthancDicomAet_ = Toolbox::NormalizeAet(dicomAet); + } + + std::string OrthancConfiguration::GetOrthancAET() const + { + if (orthancDicomAet_.empty()) + { + throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls); + } + + return orthancDicomAet_; + } + void OrthancConfiguration::LoadPeers() { if (GetBooleanParameter(ORTHANC_PEERS_IN_DB)) @@ -371,9 +386,9 @@ else { // Peers are stored in the configuration files - if (json_.isMember(ORTHANC_PEERS)) + if (userConfiguration_.isMember(ORTHANC_PEERS)) { - LoadPeersFromJson(json_[ORTHANC_PEERS]); + LoadPeersFromJson(userConfiguration_[ORTHANC_PEERS]); } else { @@ -437,9 +452,9 @@ { // Modalities are stored in the configuration files if (!modalities_.empty() || - json_.isMember(DICOM_MODALITIES)) + userConfiguration_.isMember(DICOM_MODALITIES)) { - SaveModalitiesToJson(json_[DICOM_MODALITIES]); + SaveModalitiesToJson(userConfiguration_[DICOM_MODALITIES]); } } } @@ -469,9 +484,9 @@ { // Peers are stored in the configuration files if (!peers_.empty() || - json_.isMember(ORTHANC_PEERS)) + userConfiguration_.isMember(ORTHANC_PEERS)) { - SavePeersToJson(json_[ORTHANC_PEERS]); + SavePeersToJson(userConfiguration_[ORTHANC_PEERS]); } } } @@ -484,19 +499,20 @@ } - bool OrthancConfiguration::LookupStringParameter(std::string& target, - const std::string& parameter) const + static bool LookupStringParameterInternal(std::string& target, + const Json::Value& config, + const std::string& parameter) { - if (json_.isMember(parameter)) + if (config.isMember(parameter)) { - if (json_[parameter].type() != Json::stringValue) + if (config[parameter].type() != Json::stringValue) { throw OrthancException(ErrorCode_BadParameterType, "The configuration option \"" + parameter + "\" must be a string"); } else { - target = json_[parameter].asString(); + target = config[parameter].asString(); return true; } } @@ -507,6 +523,13 @@ } + bool OrthancConfiguration::LookupStringParameter(std::string& target, + const std::string& parameter) const + { + return LookupStringParameterInternal(target, userConfiguration_, parameter); + } + + std::string OrthancConfiguration::GetStringParameter(const std::string& parameter) const { std::string value; @@ -514,30 +537,32 @@ { return value; } - else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::stringValue) + else if (LookupStringParameterInternal(value, defaultConfiguration_, parameter)) { - return defaultConfiguration[parameter].asString(); + return value; } else { - throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'"); + throw OrthancException(ErrorCode_InternalError, + "No or invalid default parameter found in the default configuration for '" + parameter + "'"); } } - - bool OrthancConfiguration::LookupIntegerParameter(int& target, - const std::string& parameter) const + + static bool LookupIntegerParameterInternal(int& target, + const Json::Value& config, + const std::string& parameter) { - if (json_.isMember(parameter)) + if (config.isMember(parameter)) { - if (json_[parameter].type() != Json::intValue) + if (config[parameter].type() != Json::intValue) { throw OrthancException(ErrorCode_BadParameterType, "The configuration option \"" + parameter + "\" must be an integer"); } else { - target = json_[parameter].asInt(); + target = config[parameter].asInt(); return true; } } @@ -548,13 +573,41 @@ } - - bool OrthancConfiguration::LookupUnsignedIntegerParameter(unsigned int& target, - const std::string& parameter) const + bool OrthancConfiguration::LookupIntegerParameter(int& target, + const std::string& parameter) const + { + return LookupIntegerParameterInternal(target, userConfiguration_, parameter); + } + + + bool OrthancConfiguration::GetIntegerParameter(int& target, + const std::string& parameter) const { int v; + if (LookupIntegerParameter(v, parameter)) { + return v; + } + else if (LookupIntegerParameterInternal(v, defaultConfiguration_, parameter)) + { + return v; + } + else + { + throw OrthancException(ErrorCode_InternalError, + "No or invalid default parameter found in the default configuration for '" + parameter + "'"); + } + } + + + static bool LookupUnsignedIntegerParameterInternal(unsigned int& target, + const Json::Value& config, + const std::string& parameter) + { + int v; + if (LookupIntegerParameterInternal(v, config, parameter)) + { if (v < 0) { throw OrthancException(ErrorCode_ParameterOutOfRange, @@ -573,6 +626,13 @@ } + bool OrthancConfiguration::LookupUnsignedIntegerParameter(unsigned int& target, + const std::string& parameter) const + { + return LookupUnsignedIntegerParameterInternal(target, userConfiguration_, parameter); + } + + unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(const std::string& parameter) const { unsigned int v; @@ -580,23 +640,25 @@ { return v; } - else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::intValue) + else if (LookupUnsignedIntegerParameterInternal(v, defaultConfiguration_, parameter)) { - return static_cast(defaultConfiguration[parameter].asInt()); + return v; } else { - throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'"); + throw OrthancException(ErrorCode_InternalError, + "No or invalid default parameter found in the default configuration for '" + parameter + "'"); } } - bool OrthancConfiguration::LookupBooleanParameter(bool& target, - const std::string& parameter) const + static bool LookupBooleanParameterInternal(bool& target, + const Json::Value& config, + const std::string& parameter) { - if (json_.isMember(parameter)) + if (config.isMember(parameter)) { - if (json_[parameter].type() != Json::booleanValue) + if (config[parameter].type() != Json::booleanValue) { throw OrthancException(ErrorCode_BadParameterType, "The configuration option \"" + parameter + @@ -604,7 +666,7 @@ } else { - target = json_[parameter].asBool(); + target = config[parameter].asBool(); return true; } } @@ -615,6 +677,13 @@ } + bool OrthancConfiguration::LookupBooleanParameter(bool& target, + const std::string& parameter) const + { + return LookupBooleanParameterInternal(target, userConfiguration_, parameter); + } + + bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter) const { bool value; @@ -622,52 +691,76 @@ { return value; } - else if (defaultConfiguration.isMember(parameter) && defaultConfiguration[parameter].type() == Json::booleanValue) + else if (LookupBooleanParameterInternal(value, defaultConfiguration_, parameter)) { - return defaultConfiguration[parameter].asBool(); + return value; } else { - throw OrthancException(ErrorCode_InternalError, std::string("No or invalid default parameter found in the default configuration for '") + parameter + "'"); + throw OrthancException(ErrorCode_InternalError, + "No or invalid default parameter found in the default configuration for '" + parameter + "'"); } } - void OrthancConfiguration::Read(const boost::filesystem::path& configurationFile) + std::string OrthancConfiguration::GetConfigurationAbsolutePath() const { + if (configurationPaths_.empty()) + { + return ""; + } + else + { + if (configurationPaths_.size() >= 2) + { + LOG(WARNING) << "A plugin has called the deprecated OrthancPluginGetConfigurationPath() primitive, the results " + << "are unreliable because multiple configuration paths were provided"; + } + + const boost::filesystem::path& first = configurationPaths_.front(); + + if (boost::filesystem::is_directory(first)) + { + return boost::filesystem::absolute(first).parent_path().string(); + } + else + { + return boost::filesystem::absolute(first).string(); + } + } + } + + + void OrthancConfiguration::Read(const std::list& configurationPaths) + { + if (configurationPaths.empty()) + { + LOG(WARNING) << "Using the default Orthanc configuration"; + } + // Read the content of the configuration - configurationFileArg_ = configurationFile; - ReadConfiguration(json_, configurationFile); + configurationPaths_ = configurationPaths; + ReadConfiguration(userConfiguration_, configurationPaths); // Adapt the paths to the configurations defaultDirectory_ = boost::filesystem::current_path(); - configurationAbsolutePath_ = ""; - if (configurationFile.empty()) + if (!configurationPaths.empty()) { - if (boost::filesystem::is_directory(configurationFile)) + const boost::filesystem::path& first = configurationPaths.front(); + + if (boost::filesystem::is_directory(first)) { - defaultDirectory_ = configurationFile; - configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).parent_path().string(); + defaultDirectory_ = first; } else { - defaultDirectory_ = boost::filesystem::path(configurationFile).parent_path(); - configurationAbsolutePath_ = boost::filesystem::absolute(configurationFile).string(); + defaultDirectory_ = boost::filesystem::path(first).parent_path(); } } - else - { -#if ORTHANC_STANDALONE != 1 - // In a non-standalone build, we use the - // "Resources/Configuration.json" from the Orthanc source code - boost::filesystem::path p = ORTHANC_PATH; - p /= "Resources"; - p /= "Configuration.json"; - configurationAbsolutePath_ = boost::filesystem::absolute(p).string(); -#endif - } + LOG(WARNING) << "The paths in the Orthanc configuration will be interpreted relative to folder: " + << SystemToolbox::PathToUtf8(defaultDirectory_); } @@ -763,13 +856,13 @@ httpServer.ClearUsers(); - if (!json_.isMember(REGISTERED_USERS)) + if (!userConfiguration_.isMember(REGISTERED_USERS)) { return RegisteredUsersStatus_NoConfiguration; } else { - const Json::Value& users = json_[REGISTERED_USERS]; + const Json::Value& users = userConfiguration_[REGISTERED_USERS]; if (users.type() != Json::objectValue) { throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users"); @@ -812,12 +905,12 @@ { target.clear(); - if (!json_.isMember(key)) + if (!userConfiguration_.isMember(key)) { return; } - const Json::Value& lst = json_[key]; + const Json::Value& lst = userConfiguration_[key]; if (lst.type() != Json::arrayValue) { @@ -836,12 +929,12 @@ { target.clear(); - if (!json_.isMember(key)) + if (!userConfiguration_.isMember(key)) { return; } - const Json::Value& lst = json_[key]; + const Json::Value& lst = userConfiguration_[key]; if (lst.type() != Json::arrayValue) { @@ -1014,7 +1107,7 @@ void OrthancConfiguration::Format(std::string& result) const { - Toolbox::WriteStyledJson(result, json_); + Toolbox::WriteStyledJson(result, userConfiguration_); } @@ -1024,17 +1117,17 @@ // Propagate the encoding to the configuration file that is // stored in memory - json_["DefaultEncoding"] = EnumerationToString(encoding); + userConfiguration_["DefaultEncoding"] = EnumerationToString(encoding); } bool OrthancConfiguration::HasConfigurationChanged() const { Json::Value current; - ReadConfiguration(current, configurationFileArg_); + ReadConfiguration(current, configurationPaths_); std::string a, b; - Toolbox::WriteFastJson(a, json_); + Toolbox::WriteFastJson(a, userConfiguration_); Toolbox::WriteFastJson(b, current); return a != b; @@ -1116,9 +1209,9 @@ static const char* const ACCEPTED_TRANSFER_SYNTAXES = "AcceptedTransferSyntaxes"; - if (json_.isMember(ACCEPTED_TRANSFER_SYNTAXES)) + if (userConfiguration_.isMember(ACCEPTED_TRANSFER_SYNTAXES)) { - ParseAcceptedTransferSyntaxes(target, json_[ACCEPTED_TRANSFER_SYNTAXES]); + ParseAcceptedTransferSyntaxes(target, userConfiguration_[ACCEPTED_TRANSFER_SYNTAXES]); } else { @@ -1205,9 +1298,9 @@ void OrthancConfiguration::LoadWarnings() { - if (json_.isMember(WARNINGS)) + if (userConfiguration_.isMember(WARNINGS)) { - const Json::Value& warnings = json_[WARNINGS]; + const Json::Value& warnings = userConfiguration_[WARNINGS]; if (!warnings.isObject()) { throw OrthancException(ErrorCode_BadFileFormat, std::string(WARNINGS) + " configuration entry is not a Json object"); diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Sources/OrthancConfiguration.h --- a/OrthancServer/Sources/OrthancConfiguration.h Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Sources/OrthancConfiguration.h Wed Aug 12 08:33:57 2026 +0200 @@ -102,22 +102,20 @@ typedef std::map Peers; typedef std::map JobsEngineThreadsCount; - boost::shared_mutex mutex_; - Json::Value json_; - boost::filesystem::path defaultDirectory_; - std::string configurationAbsolutePath_; - FontRegistry fontRegistry_; - boost::filesystem::path configurationFileArg_; - Modalities modalities_; - Peers peers_; - JobsEngineThreadsCount jobsEngineThreadsCount_; - ServerIndex* serverIndex_; - std::set disabledWarnings_; + boost::shared_mutex mutex_; + Json::Value defaultConfiguration_; + std::list configurationPaths_; + Json::Value userConfiguration_; + boost::filesystem::path defaultDirectory_; + FontRegistry fontRegistry_; + Modalities modalities_; + Peers peers_; + JobsEngineThreadsCount jobsEngineThreadsCount_; + ServerIndex* serverIndex_; + std::set disabledWarnings_; + std::string orthancDicomAet_; - OrthancConfiguration() : - serverIndex_(NULL) - { - } + OrthancConfiguration(); void LoadModalitiesFromJson(const Json::Value& source); @@ -158,7 +156,7 @@ const Json::Value& GetJson() const { - return configuration_.json_; + return configuration_.userConfiguration_; } }; @@ -188,22 +186,19 @@ const Json::Value& GetJson() const { - return configuration_.json_; + return configuration_.userConfiguration_; } }; - const std::string& GetConfigurationAbsolutePath() const - { - return configurationAbsolutePath_; - } + std::string GetConfigurationAbsolutePath() const; const FontRegistry& GetFontRegistry() const { return fontRegistry_; } - void Read(const boost::filesystem::path &configurationFile); + void Read(const std::list& configurationPaths); // "SetServerIndex()" must have been called void LoadModalitiesAndPeers(); @@ -224,6 +219,9 @@ bool LookupIntegerParameter(int& target, const std::string& parameter) const; + bool GetIntegerParameter(int& target, + const std::string& parameter) const; + bool LookupUnsignedIntegerParameter(unsigned int& target, const std::string& parameter) const; @@ -313,10 +311,9 @@ return disabledWarnings_.count(warning) == 0; } - std::string GetOrthancAET() const - { - return GetStringParameter(ORTHANC_CONFIG_DICOM_AET); - } + void LoadOrthancAET(); + + std::string GetOrthancAET() const; std::string GetOrthancName() const { diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Sources/OrthancInitialization.cpp --- a/OrthancServer/Sources/OrthancInitialization.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -331,7 +331,7 @@ - void OrthancInitialize(const boost::filesystem::path& configurationFile) + void OrthancInitialize(const std::list& configurationPaths) { static const char* const LOCALE = "Locale"; static const char* const PKCS11 = "Pkcs11"; @@ -348,7 +348,8 @@ InitializeServerEnumerations(); // Read the user-provided configuration - lock.GetConfiguration().Read(configurationFile); + lock.GetConfiguration().Read(configurationPaths); + lock.GetConfiguration().LoadOrthancAET(); // read it early because we need it for the ServerDatabaseIdentifier { std::string locale; @@ -405,6 +406,12 @@ } + void OrthancInitialize() + { + std::list configurationPaths; + OrthancInitialize(configurationPaths); + } + void OrthancFinalize() { diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Sources/OrthancInitialization.h --- a/OrthancServer/Sources/OrthancInitialization.h Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Sources/OrthancInitialization.h Wed Aug 12 08:33:57 2026 +0200 @@ -30,7 +30,9 @@ namespace Orthanc { - void OrthancInitialize(const boost::filesystem::path& configurationFile); + void OrthancInitialize(const std::list& configurationPaths); + + void OrthancInitialize(); // Use only the default configuration void OrthancFinalize(); diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Sources/ServerJobs/ArchiveJob.cpp --- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -1404,7 +1404,7 @@ // New in Orthanc 1.12.11 value[KEY_ALLOW_UTF8] = allowUtf8_; - // New in Orthanc 1.12.12 + // New in Orthanc 1.13.0 if (hasLossyQuality_) { value[KEY_LOSSY_QUALITY] = lossyQuality_; diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/Sources/main.cpp --- a/OrthancServer/Sources/main.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/Sources/main.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -57,6 +57,8 @@ #include "StorageCommitmentReports.h" #include +#include + #include // For STATUS_STORE_Error_CannotUnderstand #if defined(_WIN32) || defined(__CYGWIN__) @@ -727,13 +729,14 @@ }; -static void PrintHelp(const boost::filesystem::path& path) +static void PrintHelp(const boost::filesystem::path& path, + const boost::program_options::options_description& all) { std::cout << "Usage: " << SystemToolbox::PathToUtf8(path) << " [OPTION]... [CONFIGURATION]" << std::endl << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl << std::endl - << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl + << "The \"CONFIGURATION\" argument can be a list of files or directories. In the " << std::endl << "case of a directory, all the JSON files it contains will be merged. " << std::endl << "If no configuration path is given on the command line, a set of default " << std::endl << "parameters is used. Please refer to the Orthanc Book for the full " << std::endl @@ -744,43 +747,9 @@ << "reset the value of other log-related options that were read before." << std::endl << std::endl << "The recommended set of options to debug DICOM communications is " << std::endl - << "\"--verbose --trace-dicom --logfile=dicom.log\"" << std::endl - << std::endl - << "Command-line options:" << std::endl - << " --help\t\tdisplay this help and exit" << std::endl - << " --logdir=[dir]\tdirectory where to store the log files" << std::endl - << "\t\t\t(by default, the log is dumped to stderr)" << std::endl - << " --logfile=[file]\tfile where to store the log of Orthanc" << std::endl - << "\t\t\t(by default, the log is dumped to stderr)" << std::endl - << " --config=[file]\tcreate a sample configuration file and exit" << std::endl - << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl - << " --errors\t\tprint the supported error codes and exit" << std::endl - << " --verbose\t\tbe verbose in logs" << std::endl - << " --logs-no-thread\tto remove thread names from logs" << std::endl - << " --logs-no-context\tto remove contexts from logs" << std::endl - << " --logs-threadnames-in-context\t\tto add caller thread names in logs contexts" << std::endl - << " --trace\t\thighest verbosity in logs (for debug)" << std::endl - << " --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl - << "\t\t\tdatabase (beware that the database will become" << std::endl - << "\t\t\tincompatible with former versions of Orthanc)" << std::endl - << " --no-jobs\t\tdon't restart the jobs that were stored during" << std::endl - << "\t\t\tthe last execution of Orthanc" << std::endl - << " --openapi=[file]\twrite the OpenAPI documentation and exit" << std::endl - << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl - << " --cheatsheet=[file]\twrite the cheat sheet of REST API as CSV" << std::endl - << "\t\t\tand exit (if \"file\" is \"-\", dumps to stdout)" << std::endl - << " --version\t\toutput version information and exit" << std::endl - << std::endl - << "Fine-tuning of log categories:" << std::endl; + << "\"--verbose --trace-dicom --logfile=dicom.log\"" << std::endl; - for (size_t i = 0; i < Logging::GetCategoriesCount(); i++) - { - const std::string name = Logging::GetCategoryName(i); - std::cout << " --verbose-" << name - << "\tbe verbose in logs of category \"" << name << "\"" << std::endl; - std::cout << " --trace-" << name - << "\tuse highest verbosity for logs of category \"" << name << "\"" << std::endl; - } + std::cout << all; std::cout << std::endl @@ -1731,7 +1700,13 @@ if (context.IsReadOnly()) { - LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: " << ORTHANC_CONFIG_STORAGE_COMPRESSION << ", " << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS << ", " << ORTHANC_CONFIG_OVERWRITE_INSTANCES << ", " << ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT << ", " << ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE <<", " << ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE << ", SaveJobs"; + LOG(WARNING) << "READ-ONLY SYSTEM: ignoring these configurations: " + << ORTHANC_CONFIG_STORAGE_COMPRESSION << ", " + << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS << ", " + << ORTHANC_CONFIG_OVERWRITE_INSTANCES << ", " + << ORTHANC_CONFIG_MAXIMUM_PATIENT_COUNT << ", " + << ORTHANC_CONFIG_MAXIMUM_STORAGE_SIZE << ", " + << ORTHANC_CONFIG_MAXIMUM_STORAGE_MODE << ", SaveJobs"; } else { @@ -1750,7 +1725,9 @@ overwriteInstancesMode = StringToOverwriteInstancesMode(strOverwriteInstancesMode); if (overwriteInstancesMode == OverwriteInstancesMode_IfChanged && !context.IsStoreMD5ForAttachments()) { - LOG(ERROR) << "Can not set \"" << ORTHANC_CONFIG_OVERWRITE_INSTANCES << "\" to \"IfChanged\" when \"" << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS << "\" is set to false."; + LOG(ERROR) << "Can not set \"" << ORTHANC_CONFIG_OVERWRITE_INSTANCES + << "\" to \"IfChanged\" when \"" << ORTHANC_CONFIG_STORE_MD5_FOR_ATTACHMENTS + << "\" is set to false."; return false; } } @@ -1982,6 +1959,128 @@ } +static int ExportOpenApi(const std::string& target) +{ + Json::Value openapi; + + { + SQLiteDatabaseWrapper inMemoryDatabase; + inMemoryDatabase.Open(); + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); + ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */, + true /* unit testing */, 0 /* max completed jobs */, false /* readonly */); + OrthancRestApi restApi(context, false /* no Orthanc Explorer */); + restApi.GenerateOpenApiDocumentation(openapi); + context.Stop(); + } + + openapi["info"]["version"] = ORTHANC_VERSION; + openapi["info"]["title"] = "Orthanc API"; + openapi["info"]["description"] = + "This is the full documentation of the [REST API](https://orthanc.uclouvain.be/book/users/rest.html) " + "of Orthanc.

This reference is automatically generated from the source code of Orthanc. A " + "[shorter cheat sheet](https://orthanc.uclouvain.be/book/users/rest-cheatsheet.html) is part of " + "the Orthanc Book.

An earlier, manually crafted version from August 2019, is [still available]" + "(2019-08-orthanc-openapi.html), but is not up-to-date anymore ([source]" + "(https://groups.google.com/g/orthanc-users/c/NUiJTEICSl8/m/xKeqMrbqAAAJ))."; + + Json::Value server = Json::objectValue; + server["url"] = "https://orthanc.uclouvain.be/demo/"; + openapi["servers"].append(server); + + std::string s; + Toolbox::WriteStyledJson(s, openapi); + + try + { + if (target == "-") + { + std::cout << s; // Print to stdout + } + else + { + SystemToolbox::WriteFile(s, SystemToolbox::PathFromUtf8(target)); + } + + return 0; + } + catch (OrthancException&) + { + LOG(ERROR) << "Cannot export OpenAPI documentation as file \"" << target << "\""; + return -1; + } +} + + +static int ExportCheatSheet(const std::string& target) +{ + std::string cheatsheet; + + { + SQLiteDatabaseWrapper inMemoryDatabase; + inMemoryDatabase.Open(); + PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); + ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */, + true /* unit testing */, 0 /* max completed jobs */, false /* readonly */); + OrthancRestApi restApi(context, false /* no Orthanc Explorer */); + restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html"); + context.Stop(); + } + + try + { + if (target == "-") + { + std::cout << cheatsheet; // Print to stdout + } + else + { + SystemToolbox::WriteFile(cheatsheet, SystemToolbox::PathFromUtf8(target)); + } + + return 0; + } + catch (OrthancException&) + { + LOG(ERROR) << "Cannot export REST cheat sheet as file \"" << target << "\""; + return -1; + } +} + + +static int ExportResource(const std::string& target, + Orthanc::ServerResources::FileResourceId resource) +{ + try + { + std::string content; + GetFileResource(content, resource); + +#if defined(_WIN32) + // Replace UNIX newlines with DOS newlines + boost::replace_all(content, "\n", "\r\n"); +#endif + + if (target == "-") + { + // New in 1.5.8: Print to stdout + std::cout << content; + } + else + { + SystemToolbox::WriteFile(content, SystemToolbox::PathFromUtf8(target)); + } + + return 0; + } + catch (OrthancException&) + { + LOG(ERROR) << "Cannot write to file " << SystemToolbox::PathFromUtf8(target) << ", aborting."; + return -1; + } +} + + #if defined(_WIN32) && !defined(__MINGW32__) // arguments are passed as UTF-16 on Windows int wmain(int argc, wchar_t *argv[]) @@ -2016,248 +2115,306 @@ bool upgradeDatabase = false; bool loadJobsFromDatabase = true; - boost::filesystem::path configurationFile; + std::list configurationPaths; + /** * Parse the command-line options. - **/ + **/ + + static const char* const OPTION_HELP = "help"; + static const char* const OPTION_VERSION = "version"; + static const char* const OPTION_CONFIG = "config"; + static const char* const OPTION_ADVANCED_CONFIG = "advanced-config"; + static const char* const OPTION_ERRORS = "errors"; + static const char* const OPTION_UPGRADE = "upgrade"; + static const char* const OPTION_NO_JOBS = "no-jobs"; + static const char* const OPTION_OPENAPI = "openapi"; + static const char* const OPTION_CHEATSHEET = "cheatsheet"; + static const char* const OPTION_INPUTS = "inputs"; + + static const char* const OPTION_LOGDIR = "logdir"; + static const char* const OPTION_LOGFILE = "logfile"; + static const char* const OPTION_VERBOSE = "verbose"; + static const char* const OPTION_TRACE = "trace"; + static const char* const OPTION_LOGS_NO_THREAD = "logs-no-thread"; + static const char* const OPTION_LOGS_NO_CONTEXT = "logs-no-context"; + static const char* const OPTION_LOGS_THREAD_NAMES_IN_CONTEXT = "logs-thread-names-in-context"; + + boost::program_options::options_description allWithoutHidden; + std::vector orderSensitiveArguments; + std::vector unrecognized; + boost::program_options::variables_map options; - for (size_t i = 1; i < arguments.size(); i++) { - const std::string& argument = arguments[i]; + boost::program_options::options_description generic("Generic options"); + generic.add_options() + (OPTION_HELP, "display this help and exit") + (OPTION_VERSION, "output version information and exit") + (OPTION_CONFIG, boost::program_options::value()->value_name("file"), + "create a sample configuration file and exit (if \"file\" is \"-\", dumps to stdout)") + (OPTION_ADVANCED_CONFIG, boost::program_options::value()->value_name("file"), + "create a file containing the advanced configuration options and exit (if \"file\" is \"-\", dumps to stdout)") + (OPTION_ERRORS, "print the supported error codes and exit") + (OPTION_UPGRADE, "allow Orthanc to upgrade the version of the database " + "(beware that the database will become incompatible with former versions of Orthanc)") + (OPTION_NO_JOBS, "don't restart the jobs that were stored during the last execution of Orthanc") + (OPTION_OPENAPI, boost::program_options::value()->value_name("file"), + "write the OpenAPI documentation and exit (if \"file\" is \"-\", dumps to stdout)") + (OPTION_CHEATSHEET, boost::program_options::value()->value_name("file"), + "write the cheat sheet of REST API as CSV and exit (if \"file\" is \"-\", dumps to stdout)"); + + boost::program_options::options_description logging("Logging options"); + logging.add_options() + (OPTION_LOGDIR, boost::program_options::value()->value_name("dir"), + "directory where to store the log files (by default, the log is dumped to stderr)") + (OPTION_LOGFILE, boost::program_options::value()->value_name("file"), + "file where to store the log of Orthanc (by default, the log is dumped to stderr)") + (OPTION_VERBOSE, "be verbose in logs") + (OPTION_TRACE, "highest verbosity in logs (for debug)") + + // New in Orthanc 1.12.12 + (OPTION_LOGS_NO_THREAD, "remove thread names from logs") - if (argument.empty()) + // New in Orthanc 1.13.0 + (OPTION_LOGS_NO_CONTEXT, "remove contexts from logs") + (OPTION_LOGS_THREAD_NAMES_IN_CONTEXT, "include caller thread names in log contexts"); + + boost::program_options::options_description finetuning("Fine-tuning of log categories"); + + std::set orderSensitiveOptions; + orderSensitiveOptions.insert(OPTION_VERBOSE); + orderSensitiveOptions.insert(OPTION_TRACE); + + for (size_t i = 0; i < Logging::GetCategoriesCount(); i++) { - // Ignore empty arguments + const std::string name = Logging::GetCategoryName(i); + const std::string verboseName = "verbose-" + name; + const std::string verboseDescription = "be verbose in logs of category \"" + name + "\""; + const std::string traceName = "trace-" + name; + const std::string traceDescription = "use highest verbosity for logs of category \"" + name + "\""; + + finetuning.add_options() (verboseName.c_str(), verboseDescription.c_str()); + finetuning.add_options() (traceName.c_str(), traceDescription.c_str()); + + orderSensitiveOptions.insert(verboseName); + orderSensitiveOptions.insert(traceName); } - else if (argument[0] != '-') + + allWithoutHidden.add(generic); + allWithoutHidden.add(logging); + allWithoutHidden.add(finetuning); + + boost::program_options::options_description hidden; + hidden.add_options() + (OPTION_INPUTS, boost::program_options::value< std::vector >(), + "Input configuration files"); + + boost::program_options::options_description all; + all.add(allWithoutHidden); + all.add(hidden); + + boost::program_options::positional_options_description positional; + positional.add(OPTION_INPUTS, -1); // -1 = all remaining positional arguments + + if (arguments.empty()) { - if (!configurationFile.empty()) + LOG(ERROR) << "The program name is not available, aborting"; + return -1; + } + + std::vector orderNonSensitiveArguments; + orderSensitiveArguments.reserve(arguments.size()); + orderNonSensitiveArguments.reserve(arguments.size()); + + for (size_t i = 1 /* skip the program name */; i < arguments.size(); i++) + { + bool isOrderSensitive = false; + + if (boost::starts_with(arguments[i], "--")) { - LOG(ERROR) << "More than one configuration path were provided on the command line, aborting"; - return -1; - } - else - { - // Use the first argument that does not start with a "-" as - // the configuration file + const std::string key = arguments[i].substr(2); - configurationFile = SystemToolbox::PathFromUtf8(argument); -// // TODO WHAT IS THE ENCODING? -//#if defined(_WIN32) -// //configurationFileUtf8Str = SystemToolbox::WStringToUtf8(SystemToolbox::WStringFromCharPtr(argv[i])); -//#else -// configurationFileUtf8Str = std::string(argv[i]); -//#endif + if (orderSensitiveOptions.find(key) != orderSensitiveOptions.end()) + { + isOrderSensitive = true; + orderSensitiveArguments.push_back(key); + } + } + + if (!isOrderSensitive) + { + orderNonSensitiveArguments.push_back(arguments[i]); } } - else if (argument == "--errors") + + try { - PrintErrors(SystemToolbox::PathFromUtf8(arguments[0])); - return 0; + boost::program_options::parsed_options parsed = boost::program_options::command_line_parser(orderNonSensitiveArguments) + .options(all) + .positional(positional) + .allow_unregistered() // don't throw on unknown options + .run(); + + // Pull out everything that didn't match a declared option + unrecognized = boost::program_options::collect_unrecognized(parsed.options, boost::program_options::exclude_positional); + + boost::program_options::store(parsed, options); + boost::program_options::notify(options); } - else if (argument == "--help") + catch (boost::program_options::error& e) { - PrintHelp(SystemToolbox::PathFromUtf8(arguments[0])); - return 0; + LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what(); + return -1; } - else if (argument == "--version") - { - PrintVersion(SystemToolbox::PathFromUtf8(arguments[0])); - return 0; - } - else if (argument == "--verbose") + } + + + /** + * Process the command-line options. + **/ + + for (size_t i = 0; i < unrecognized.size(); i++) + { + LOG(WARNING) << "Option unsupported by the core of Orthanc: " << unrecognized[i]; + } + + for (size_t i = 0; i < orderSensitiveArguments.size(); i++) + { + const std::string& arg = orderSensitiveArguments[i]; + + if (arg == OPTION_VERBOSE) { SetGlobalVerbosity(Verbosity_Verbose); } - else if (argument == "--logs-no-thread") - { - Logging::SetThreadNamesEnabled(false); - } - else if (argument == "--logs-no-context") - { - Logging::SetThreadContextsEnabled(false); - } - else if (argument == "--logs-threadnames-in-context") - { - Logging::SetThreadNamesInContextsEnabled(true); - } - else if (argument == "--trace") + else if (arg == OPTION_TRACE) { SetGlobalVerbosity(Verbosity_Trace); } - else if (boost::starts_with(argument, "--verbose-") && - SetCategoryVerbosity(Verbosity_Verbose, argument.substr(10))) - { - // New in Orthanc 1.8.1 - } - else if (boost::starts_with(argument, "--trace-") && - SetCategoryVerbosity(Verbosity_Trace, argument.substr(8))) + else if (boost::starts_with(arg, "verbose-") && + SetCategoryVerbosity(Verbosity_Verbose, arg.substr(arg.find('-') + 1))) { // New in Orthanc 1.8.1 } - else if (boost::starts_with(argument, "--logdir=")) - { - // TODO WHAT IS THE ENCODING? - const std::string directory = argument.substr(9); - - try - { - Logging::SetTargetFolder(directory); - } - catch (OrthancException&) - { - LOG(ERROR) << "The directory where to store the log files (" - << directory << ") is inexistent, aborting."; - return -1; - } - } - else if (boost::starts_with(argument, "--logfile=")) - { - // TODO WHAT IS THE ENCODING? - const std::string file = argument.substr(10); - - try - { - Logging::SetTargetFile(file); - } - catch (OrthancException&) - { - LOG(ERROR) << "Cannot write to the specified log file (" - << file << "), aborting."; - return -1; - } - } - else if (argument == "--upgrade") - { - upgradeDatabase = true; - } - else if (argument == "--no-jobs") - { - loadJobsFromDatabase = false; - } - else if (boost::starts_with(argument, "--config=")) - { - // TODO WHAT IS THE ENCODING? - std::string configurationSample; - GetFileResource(configurationSample, ServerResources::CONFIGURATION_SAMPLE); - -#if defined(_WIN32) - // Replace UNIX newlines with DOS newlines - boost::replace_all(configurationSample, "\n", "\r\n"); -#endif - - std::string target = argument.substr(9); - - try - { - if (target == "-") - { - // New in 1.5.8: Print to stdout - std::cout << configurationSample; - } - else - { - SystemToolbox::WriteFile(configurationSample, SystemToolbox::PathFromUtf8(target)); - } - return 0; - } - catch (OrthancException&) - { - LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\""; - return -1; - } - } - else if (boost::starts_with(argument, "--openapi=")) + else if (boost::starts_with(arg, "trace-") && + SetCategoryVerbosity(Verbosity_Trace, arg.substr(arg.find('-') + 1))) { - std::string target = argument.substr(10); - - try - { - Json::Value openapi; - - { - SQLiteDatabaseWrapper inMemoryDatabase; - inMemoryDatabase.Open(); - PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); - ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */, - true /* unit testing */, 0 /* max completed jobs */, false /* readonly */); - OrthancRestApi restApi(context, false /* no Orthanc Explorer */); - restApi.GenerateOpenApiDocumentation(openapi); - context.Stop(); - } - - openapi["info"]["version"] = ORTHANC_VERSION; - openapi["info"]["title"] = "Orthanc API"; - openapi["info"]["description"] = - "This is the full documentation of the [REST API](https://orthanc.uclouvain.be/book/users/rest.html) " - "of Orthanc.

This reference is automatically generated from the source code of Orthanc. A " - "[shorter cheat sheet](https://orthanc.uclouvain.be/book/users/rest-cheatsheet.html) is part of " - "the Orthanc Book.

An earlier, manually crafted version from August 2019, is [still available]" - "(2019-08-orthanc-openapi.html), but is not up-to-date anymore ([source]" - "(https://groups.google.com/g/orthanc-users/c/NUiJTEICSl8/m/xKeqMrbqAAAJ))."; - - Json::Value server = Json::objectValue; - server["url"] = "https://orthanc.uclouvain.be/demo/"; - openapi["servers"].append(server); - - std::string s; - Toolbox::WriteStyledJson(s, openapi); - - if (target == "-") - { - std::cout << s; // Print to stdout - } - else - { - SystemToolbox::WriteFile(s, SystemToolbox::PathFromUtf8(target)); - } - return 0; - } - catch (OrthancException&) - { - LOG(ERROR) << "Cannot export OpenAPI documentation as file \"" << target << "\""; - return -1; - } - } - else if (boost::starts_with(argument, "--cheatsheet=")) - { - std::string target = argument.substr(13); - - try - { - std::string cheatsheet; - - { - SQLiteDatabaseWrapper inMemoryDatabase; - inMemoryDatabase.Open(); - PluginStorageAreaAdapter inMemoryStorage(new MemoryStorageArea); - ServerContext context(inMemoryDatabase, inMemoryStorage, NULL /* no transcoder */, - true /* unit testing */, 0 /* max completed jobs */, false /* readonly */); - OrthancRestApi restApi(context, false /* no Orthanc Explorer */); - restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://orthanc.uclouvain.be/api/index.html"); - context.Stop(); - } - - if (target == "-") - { - std::cout << cheatsheet; // Print to stdout - } - else - { - SystemToolbox::WriteFile(cheatsheet, SystemToolbox::PathFromUtf8(target)); - } - return 0; - } - catch (OrthancException&) - { - LOG(ERROR) << "Cannot export REST cheat sheet as file \"" << target << "\""; - return -1; - } + // New in Orthanc 1.8.1 } else { - LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument; + // Should never happen + LOG(ERROR) << "Unsupported command-line option: " << arg; + return -1; + } + } + + if (options.count(OPTION_HELP) == 1) + { + PrintHelp(arguments[0], allWithoutHidden); + return 0; + } + + if (options.count(OPTION_VERSION) == 1) + { + PrintVersion(arguments[0]); + return 0; + } + + if (options.count(OPTION_LOGDIR) == 1) + { + const std::string directory = options[OPTION_LOGDIR].as(); + + try + { + Logging::SetTargetFolder(directory); + } + catch (OrthancException&) + { + LOG(ERROR) << "The directory where to store the log files (" + << SystemToolbox::PathFromUtf8(directory) << ") is inexistent, aborting."; + return -1; + } + } + + if (options.count(OPTION_LOGFILE) == 1) + { + const std::string file = options[OPTION_LOGFILE].as(); + + try + { + Logging::SetTargetFile(file); + } + catch (OrthancException&) + { + LOG(ERROR) << "Cannot write to the specified log file (" + << SystemToolbox::PathFromUtf8(file) << "), aborting."; + return -1; + } + } + + if (options.count(OPTION_CONFIG) == 1) + { + const std::string file = options[OPTION_CONFIG].as(); + return ExportResource(file, Orthanc::ServerResources::CONFIGURATION_SAMPLE); + } + + if (options.count(OPTION_ADVANCED_CONFIG) == 1) + { + const std::string file = options[OPTION_ADVANCED_CONFIG].as(); + return ExportResource(file, Orthanc::ServerResources::ADVANCED_CONFIGURATION_SAMPLE); + } + + if (options.count(OPTION_ERRORS) == 1) + { + PrintErrors(arguments[0]); + return 0; + } + + if (options.count(OPTION_UPGRADE) == 1) + { + upgradeDatabase = true; + } + + if (options.count(OPTION_NO_JOBS) == 1) + { + loadJobsFromDatabase = false; + } + + if (options.count(OPTION_OPENAPI) == 1) + { + const std::string file = options[OPTION_OPENAPI].as(); + return ExportOpenApi(file); + } + + if (options.count(OPTION_CHEATSHEET) == 1) + { + const std::string file = options[OPTION_CHEATSHEET].as(); + return ExportCheatSheet(file); + } + + if (options.count(OPTION_LOGS_NO_THREAD) == 1) + { + Logging::SetThreadNamesEnabled(false); + } + + if (options.count(OPTION_LOGS_NO_CONTEXT) == 1) + { + Logging::SetThreadContextsEnabled(false); + } + + if (options.count(OPTION_LOGS_THREAD_NAMES_IN_CONTEXT) == 1) + { + Logging::SetThreadNamesInContextsEnabled(true); + } + + if (options.count(OPTION_INPUTS) != 0) + { + const std::vector& inputs = options[OPTION_INPUTS].as >(); + + for (size_t i = 0; i < inputs.size(); i++) + { + configurationPaths.push_back(SystemToolbox::PathFromUtf8(inputs[i])); } } @@ -2323,7 +2480,7 @@ { for (;;) { - OrthancInitialize(configurationFile); + OrthancInitialize(configurationPaths); bool restart = StartOrthanc(arguments, upgradeDatabase, loadJobsFromDatabase); if (restart) diff -r 2a54e6538924 -r 45817b3c1664 OrthancServer/UnitTestsSources/UnitTestsMain.cpp --- a/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Mon Aug 10 14:59:05 2026 +0200 +++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp Wed Aug 12 08:33:57 2026 +0200 @@ -548,7 +548,7 @@ SetGlobalVerbosity(Verbosity_Verbose); Toolbox::DetectEndianness(); SystemToolbox::MakeDirectory(SystemToolbox::PathFromUtf8("UnitTestsResults")); - OrthancInitialize(""); + OrthancInitialize(); ::testing::InitGoogleTest(&argc, argv); int result = RUN_ALL_TESTS();