Orthanc/OrthancServer/Sources/OrthancInitialization.cpp
2025-06-23 19:07:37 +05:30

688 lines
20 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#include "PrecompiledHeadersServer.h"
#if defined(_WIN32)
// "Please include winsock2.h before windows.h"
# include <winsock2.h>
#endif
#if !defined(HAVE_MALLOPT)
# error Macro HAVE_MALLOPT must be defined
#endif
#if HAVE_MALLOPT == 1
# include <malloc.h>
#endif
// This must be *before* the inclusion of "Logging.h"
#if defined(__ORTHANC_FILE__)
// Prevents the system-wide Google Protobuf library from leaking the
// full path of this source file
# undef __FILE__
# define __FILE__ __ORTHANC_FILE__
#endif
#if ORTHANC_ENABLE_PLUGINS == 1
# include <google/protobuf/stubs/common.h>
# include <google/protobuf/any.h>
#endif
#include "OrthancInitialization.h"
#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
#include "../../OrthancFramework/Sources/FileStorage/FilesystemStorage.h"
#include "../../OrthancFramework/Sources/FileStorage/PluginStorageAreaAdapter.h"
#include "../../OrthancFramework/Sources/HttpClient.h"
#include "../../OrthancFramework/Sources/Logging.h"
#include "../../OrthancFramework/Sources/OrthancException.h"
#include "../../OrthancFramework/Sources/SerializationToolbox.h"
#include "Database/SQLiteDatabaseWrapper.h"
#include "OrthancConfiguration.h"
#include <OrthancServerResources.h>
#include <dcmtk/dcmnet/diutil.h> // For DCM_dcmnetLogger
static const char* const STORAGE_DIRECTORY = "StorageDirectory";
static const char* const ORTHANC_STORAGE = "OrthancStorage";
namespace Orthanc
{
static void RegisterUserMetadata(const Json::Value& config)
{
static const char* const USER_METADATA = "UserMetadata";
if (config.isMember(USER_METADATA))
{
const Json::Value& parameter = config[USER_METADATA];
Json::Value::Members members = parameter.getMemberNames();
for (size_t i = 0; i < members.size(); i++)
{
const std::string& name = members[i];
if (!parameter[name].isInt())
{
throw OrthancException(ErrorCode_BadParameterType,
"Not a number in this user-defined metadata: " + name);
}
int metadata = parameter[name].asInt();
LOG(INFO) << "Registering user-defined metadata: " << name << " (index "
<< metadata << ")";
try
{
RegisterUserMetadata(metadata, name);
}
catch (OrthancException&)
{
LOG(ERROR) << "Cannot register this user-defined metadata: " << name;
throw;
}
}
}
}
static void RegisterUserContentType(const Json::Value& config)
{
static const char* const USER_CONTENT_TYPE = "UserContentType";
if (config.isMember(USER_CONTENT_TYPE))
{
const Json::Value& parameter = config[USER_CONTENT_TYPE];
Json::Value::Members members = parameter.getMemberNames();
for (size_t i = 0; i < members.size(); i++)
{
const std::string& name = members[i];
std::string mime = MIME_BINARY;
const Json::Value& value = parameter[name];
int contentType;
if (value.isArray() &&
value.size() == 2 &&
value[0].isInt() &&
value[1].isString())
{
contentType = value[0].asInt();
mime = value[1].asString();
}
else if (value.isInt())
{
contentType = value.asInt();
}
else
{
throw OrthancException(ErrorCode_BadParameterType,
"Not a number in this user-defined attachment type: " + name);
}
LOG(INFO) << "Registering user-defined attachment type: " << name << " (index "
<< contentType << ") with MIME type \"" << mime << "\"";
try
{
RegisterUserContentType(contentType, name, mime);
}
catch (OrthancException&)
{
throw;
}
}
}
}
static void LoadExternalDictionaries(const Json::Value& configuration)
{
static const char* const EXTERNAL_DICTIONARIES = "ExternalDictionaries";
if (configuration.type() == Json::objectValue &&
configuration.isMember(EXTERNAL_DICTIONARIES))
{
std::vector<std::string> dictionaries;
SerializationToolbox::ReadArrayOfStrings(dictionaries, configuration, EXTERNAL_DICTIONARIES);
FromDcmtkBridge::LoadExternalDictionaries(dictionaries);
}
}
static void LoadCustomDictionary(const Json::Value& configuration)
{
static const char* const DICTIONARY = "Dictionary";
if (configuration.type() != Json::objectValue ||
!configuration.isMember(DICTIONARY) ||
configuration[DICTIONARY].type() != Json::objectValue)
{
return;
}
Json::Value::Members tags(configuration[DICTIONARY].getMemberNames());
for (Json::Value::ArrayIndex i = 0; i < tags.size(); i++)
{
const Json::Value& content = configuration[DICTIONARY][tags[i]];
if (content.type() != Json::arrayValue ||
content.size() < 2 ||
content.size() > 5 ||
content[0].type() != Json::stringValue ||
content[1].type() != Json::stringValue ||
(content.size() >= 3 && content[2].type() != Json::intValue) ||
(content.size() >= 4 && content[3].type() != Json::intValue) ||
(content.size() >= 5 && content[4].type() != Json::stringValue))
{
throw OrthancException(ErrorCode_BadFileFormat, "The definition of the '" + tags[i] + "' dictionary entry is invalid.");
}
DicomTag tag(FromDcmtkBridge::ParseTag(tags[i]));
ValueRepresentation vr = StringToValueRepresentation(content[0].asString(), true);
std::string name = content[1].asString();
unsigned int minMultiplicity = (content.size() >= 2) ? content[2].asUInt() : 1;
unsigned int maxMultiplicity = (content.size() >= 3) ? content[3].asUInt() : 1;
std::string privateCreator = (content.size() >= 4) ? content[4].asString() : "";
FromDcmtkBridge::RegisterDictionaryTag(tag, vr, name, minMultiplicity, maxMultiplicity, privateCreator);
}
}
static void LoadMainDicomTags(const Json::Value& configuration)
{
static const char* const EXTRA_MAIN_DICOM_TAGS = "ExtraMainDicomTags";
DicomMap::ResetDefaultMainDicomTags();
if (configuration.type() != Json::objectValue ||
!configuration.isMember(EXTRA_MAIN_DICOM_TAGS) ||
configuration[EXTRA_MAIN_DICOM_TAGS].type() != Json::objectValue)
{
return;
}
Json::Value::Members levels(configuration[EXTRA_MAIN_DICOM_TAGS].getMemberNames());
for (Json::Value::ArrayIndex i = 0; i < levels.size(); i++)
{
ResourceType level = StringToResourceType(levels[i].c_str());
const Json::Value& content = configuration[EXTRA_MAIN_DICOM_TAGS][levels[i]];
if (content.type() != Json::arrayValue)
{
throw OrthancException(ErrorCode_BadFileFormat, "The definition of the '" + levels[i] + "' ExtraMainDicomTags entry is invalid (not an array).");
}
if (content.size() > 0)
{
LOG(INFO) << "Configured Extra Main Dicom Tags for " << levels[i] << ":";
for (Json::Value::ArrayIndex t = 0; t < content.size(); t++)
{
const std::string& tagName = content[t].asString();
DicomTag tag(FromDcmtkBridge::ParseTag(tagName));
if (DicomMap::IsComputedTag(tag))
{
LOG(WARNING) << " - " << tagName << " cannot be added in the Extra Main Dicom Tags since the value of this tag is computed when requested";
}
else
{
ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
if (vr == ValueRepresentation_Sequence)
{
LOG(INFO) << " - " << tagName << " (sequence)";
}
else
{
LOG(INFO) << " - " << tagName;
}
try
{
DicomMap::AddMainDicomTag(tag, level);
}
catch(OrthancException& e)
{
LOG(WARNING) << " - !!! " << tagName << " is already defined as a standard MainDicomTags, it is useless to include it in the ExtraMainDicomTags";
}
}
}
}
}
}
static void ConfigurePkcs11(const Json::Value& config)
{
static const char* const MODULE = "Module";
static const char* const VERBOSE = "Verbose";
static const char* const PIN = "Pin";
if (config.type() != Json::objectValue ||
!config.isMember(MODULE) ||
config[MODULE].type() != Json::stringValue)
{
throw OrthancException(ErrorCode_BadFileFormat,
"No path to the PKCS#11 module (DLL or .so) is provided "
"for HTTPS client authentication");
}
std::string pin;
if (config.isMember(PIN))
{
if (config[PIN].type() == Json::stringValue)
{
pin = config[PIN].asString();
}
else
{
throw OrthancException(ErrorCode_BadFileFormat,
"The PIN number in the PKCS#11 configuration must be a string");
}
}
bool verbose = false;
if (config.isMember(VERBOSE))
{
if (config[VERBOSE].type() == Json::booleanValue)
{
verbose = config[VERBOSE].asBool();
}
else
{
throw OrthancException(ErrorCode_BadFileFormat,
"The Verbose option in the PKCS#11 configuration must be a Boolean");
}
}
HttpClient::InitializePkcs11(config[MODULE].asString(), pin, verbose);
}
void OrthancInitialize(const char* configurationFile)
{
static const char* const LOCALE = "Locale";
static const char* const PKCS11 = "Pkcs11";
static const char* const DEFAULT_ENCODING = "DefaultEncoding";
static const char* const MALLOC_ARENA_MAX = "MallocArenaMax";
static const char* const LOAD_PRIVATE_DICTIONARY = "LoadPrivateDictionary";
OrthancConfiguration::WriterLock lock;
#if ORTHANC_ENABLE_PLUGINS == 1
GOOGLE_PROTOBUF_VERIFY_VERSION;
#endif
InitializeServerEnumerations();
// Read the user-provided configuration
lock.GetConfiguration().Read(configurationFile);
{
std::string locale;
if (lock.GetJson().isMember(LOCALE))
{
locale = lock.GetConfiguration().GetStringParameter(LOCALE, "");
}
bool loadPrivate = lock.GetConfiguration().GetBooleanParameter(LOAD_PRIVATE_DICTIONARY, true);
Orthanc::InitializeFramework(locale, loadPrivate);
}
// The Orthanc framework is now initialized
if (lock.GetJson().isMember(DEFAULT_ENCODING))
{
std::string encoding = lock.GetConfiguration().GetStringParameter(DEFAULT_ENCODING, "");
SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
}
else
{
SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
}
if (lock.GetJson().isMember(PKCS11))
{
ConfigurePkcs11(lock.GetJson()[PKCS11]);
}
RegisterUserMetadata(lock.GetJson());
RegisterUserContentType(lock.GetJson());
LoadExternalDictionaries(lock.GetJson()); // New in Orthanc 1.9.4
LoadCustomDictionary(lock.GetJson());
lock.GetConfiguration().LoadWarnings();
lock.GetConfiguration().LoadJobsEngineThreadsCount();
LoadMainDicomTags(lock.GetJson()); // New in Orthanc 1.11.0
lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16);
#if HAVE_MALLOPT == 1
// New in Orthanc 1.8.2
// https://orthanc.uclouvain.be/book/faq/scalability.html#controlling-memory-usage
unsigned int maxArena = lock.GetConfiguration().GetUnsignedIntegerParameter(MALLOC_ARENA_MAX, 5);
if (maxArena != 0)
{
// https://man7.org/linux/man-pages/man3/mallopt.3.html
LOG(INFO) << "Calling mallopt(M_ARENA_MAX, " << maxArena << ")";
if (mallopt(M_ARENA_MAX, maxArena) != 1 /* success */)
{
throw OrthancException(ErrorCode_InternalError, "The call to mallopt(M_ARENA_MAX, " +
boost::lexical_cast<std::string>(maxArena) + ") has failed");
}
}
#else
if (lock.GetJson().isMember(MALLOC_ARENA_MAX))
{
LOG(INFO) << "Your platform does not support mallopt(), ignoring configuration option \""
<< MALLOC_ARENA_MAX << "\"";
}
#endif
}
void OrthancFinalize()
{
OrthancConfiguration::WriterLock lock;
Orthanc::FinalizeFramework();
#if ORTHANC_ENABLE_PLUGINS == 1
google::protobuf::ShutdownProtobufLibrary();
#endif
}
static IDatabaseWrapper* CreateSQLiteWrapper()
{
OrthancConfiguration::ReaderLock lock;
std::string storageDirectoryStr =
lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);
// Open the database
boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr));
LOG(WARNING) << "SQLite index directory: " << indexDirectory;
try
{
boost::filesystem::create_directories(indexDirectory);
}
catch (boost::filesystem::filesystem_error&)
{
}
return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index");
}
namespace
{
// Anonymous namespace to avoid clashes between compilation modules
class FilesystemStorageWithoutDicom : public IStorageArea
{
private:
FilesystemStorage storage_;
public:
FilesystemStorageWithoutDicom(const std::string& path,
bool fsyncOnWrite) :
storage_(path, fsyncOnWrite)
{
}
virtual void Create(const std::string& uuid,
const void* content,
size_t size,
FileContentType type) ORTHANC_OVERRIDE
{
if (type != FileContentType_Dicom)
{
storage_.Create(uuid, content, size, type);
}
}
virtual IMemoryBuffer* ReadRange(const std::string& uuid,
FileContentType type,
uint64_t start /* inclusive */,
uint64_t end /* exclusive */) ORTHANC_OVERRIDE
{
if (type != FileContentType_Dicom)
{
return storage_.ReadRange(uuid, type, start, end);
}
else
{
throw OrthancException(ErrorCode_UnknownResource);
}
}
virtual bool HasEfficientReadRange() const ORTHANC_OVERRIDE
{
return storage_.HasEfficientReadRange();
}
virtual void Remove(const std::string& uuid,
FileContentType type) ORTHANC_OVERRIDE
{
if (type != FileContentType_Dicom)
{
storage_.Remove(uuid, type);
}
}
};
}
static IPluginStorageArea* CreateFilesystemStorage()
{
static const char* const SYNC_STORAGE_AREA = "SyncStorageArea";
static const char* const STORE_DICOM = "StoreDicom";
OrthancConfiguration::ReaderLock lock;
std::string storageDirectoryStr =
lock.GetConfiguration().GetStringParameter(STORAGE_DIRECTORY, ORTHANC_STORAGE);
boost::filesystem::path storageDirectory =
lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);
LOG(WARNING) << "Storage directory: " << storageDirectory;
// New in Orthanc 1.7.4
bool fsyncOnWrite = lock.GetConfiguration().GetBooleanParameter(SYNC_STORAGE_AREA, true);
if (lock.GetConfiguration().GetBooleanParameter(STORE_DICOM, true))
{
return new PluginStorageAreaAdapter(new FilesystemStorage(storageDirectory.string(), fsyncOnWrite));
}
else
{
LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode";
return new PluginStorageAreaAdapter(new FilesystemStorageWithoutDicom(storageDirectory.string(), fsyncOnWrite));
}
}
IDatabaseWrapper* CreateDatabaseWrapper()
{
return CreateSQLiteWrapper();
}
IPluginStorageArea* CreateStorageArea()
{
return CreateFilesystemStorage();
}
static void SetDcmtkVerbosity(Verbosity verbosity)
{
// INFO_LOG_LEVEL was the DCMTK log level in Orthanc <= 1.8.0
// https://support.dcmtk.org/docs-dcmrt/classOFLogger.html#ae20bf2616f15313c1f089da2eefb8245
OFLogger::LogLevel dataLevel, networkLevel;
switch (verbosity)
{
case Verbosity_Default:
// Turn off logging in DCMTK core
dataLevel = OFLogger::OFF_LOG_LEVEL;
networkLevel = OFLogger::OFF_LOG_LEVEL;
break;
case Verbosity_Verbose:
dataLevel = OFLogger::INFO_LOG_LEVEL;
networkLevel = OFLogger::INFO_LOG_LEVEL;
break;
case Verbosity_Trace:
dataLevel = OFLogger::INFO_LOG_LEVEL; // DEBUG here makes DCMTK too verbose to be useful
networkLevel = OFLogger::DEBUG_LOG_LEVEL;
break;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
OFLog::configure(dataLevel);
assert(dcmtk::log4cplus::Logger::getRoot().getChainedLogLevel() == dataLevel);
DCM_dcmdataLogger.setLogLevel(dataLevel); // This seems to be implied by "OFLog::configure()"
DCM_dcmnetLogger.setLogLevel(networkLevel); // This will display PDU in DICOM networking
}
void SetGlobalVerbosity(Verbosity verbosity)
{
SetDcmtkVerbosity(verbosity);
switch (verbosity)
{
case Verbosity_Default:
Logging::EnableInfoLevel(false);
Logging::EnableTraceLevel(false);
break;
case Verbosity_Verbose:
Logging::EnableInfoLevel(true);
Logging::EnableTraceLevel(false);
break;
case Verbosity_Trace:
Logging::EnableInfoLevel(true);
Logging::EnableTraceLevel(true);
break;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
}
Verbosity GetGlobalVerbosity()
{
if (Logging::IsTraceLevelEnabled())
{
return Verbosity_Trace;
}
else if (Logging::IsInfoLevelEnabled())
{
return Verbosity_Verbose;
}
else
{
return Verbosity_Default;
}
}
void SetCategoryVerbosity(Logging::LogCategory category,
Verbosity verbosity)
{
switch (verbosity)
{
case Verbosity_Default:
Logging::SetCategoryEnabled(Logging::LogLevel_INFO, category, false);
Logging::SetCategoryEnabled(Logging::LogLevel_TRACE, category, false);
break;
case Verbosity_Verbose:
Logging::SetCategoryEnabled(Logging::LogLevel_INFO, category, true);
Logging::SetCategoryEnabled(Logging::LogLevel_TRACE, category, false);
break;
case Verbosity_Trace:
Logging::SetCategoryEnabled(Logging::LogLevel_INFO, category, true);
Logging::SetCategoryEnabled(Logging::LogLevel_TRACE, category, true);
break;
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
if (category == Logging::LogCategory_DICOM)
{
SetDcmtkVerbosity(verbosity);
}
}
Verbosity GetCategoryVerbosity(Logging::LogCategory category)
{
if (Logging::IsCategoryEnabled(Logging::LogLevel_TRACE, category))
{
return Verbosity_Trace;
}
else if (Logging::IsCategoryEnabled(Logging::LogLevel_INFO, category))
{
return Verbosity_Verbose;
}
else
{
return Verbosity_Default;
}
}
}