978 lines
32 KiB
C++
978 lines
32 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/>.
|
|
**/
|
|
|
|
|
|
#define HOUSEKEEPER_NAME "housekeeper"
|
|
|
|
#include "../Common/OrthancPluginCppWrapper.h"
|
|
#include "../../../../OrthancFramework/Sources/Compatibility.h"
|
|
|
|
#include <boost/thread.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/date_time/posix_time/posix_time.hpp>
|
|
#include <boost/date_time/special_defs.hpp>
|
|
#include <json/value.h>
|
|
#include <json/writer.h>
|
|
#include <string.h>
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <list>
|
|
#include <time.h>
|
|
|
|
static int globalPropertyId_ = 0;
|
|
static bool force_ = false;
|
|
static unsigned int throttleDelay_ = 0;
|
|
static std::unique_ptr<boost::thread> workerThread_;
|
|
static bool workerThreadShouldStop_ = false;
|
|
static bool triggerOnStorageCompressionChange_ = true;
|
|
static bool triggerOnMainDicomTagsChange_ = true;
|
|
static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
|
|
static bool triggerOnIngestTranscodingChange_ = true;
|
|
static bool triggerOnDicomWebCacheChange_ = true;
|
|
static std::string limitMainDicomTagsReconstructLevel_ = "";
|
|
static std::string limitToChange_ = "";
|
|
static std::string limitToUrl_ = "";
|
|
|
|
|
|
struct RunningPeriod
|
|
{
|
|
int fromHour_;
|
|
int toHour_;
|
|
int weekday_;
|
|
|
|
RunningPeriod(const std::string& weekday, const std::string& period)
|
|
{
|
|
if (weekday == "Monday")
|
|
{
|
|
weekday_ = 1;
|
|
}
|
|
else if (weekday == "Tuesday")
|
|
{
|
|
weekday_ = 2;
|
|
}
|
|
else if (weekday == "Wednesday")
|
|
{
|
|
weekday_ = 3;
|
|
}
|
|
else if (weekday == "Thursday")
|
|
{
|
|
weekday_ = 4;
|
|
}
|
|
else if (weekday == "Friday")
|
|
{
|
|
weekday_ = 5;
|
|
}
|
|
else if (weekday == "Saturday")
|
|
{
|
|
weekday_ = 6;
|
|
}
|
|
else if (weekday == "Sunday")
|
|
{
|
|
weekday_ = 0;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: invalid schedule: unknown 'day': " + weekday);
|
|
ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
|
|
}
|
|
|
|
std::vector<std::string> hours;
|
|
boost::split(hours, period, boost::is_any_of("-"));
|
|
|
|
fromHour_ = boost::lexical_cast<int>(hours[0]);
|
|
toHour_ = boost::lexical_cast<int>(hours[1]);
|
|
}
|
|
|
|
bool isInPeriod() const
|
|
{
|
|
time_t now = time(NULL);
|
|
tm* nowLocalTime = localtime(&now);
|
|
|
|
if (nowLocalTime->tm_wday != weekday_)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (nowLocalTime->tm_hour >= fromHour_ && nowLocalTime->tm_hour < toHour_)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
struct RunningPeriods
|
|
{
|
|
std::list<RunningPeriod> runningPeriods_;
|
|
|
|
void load(const Json::Value& scheduleConfiguration)
|
|
{
|
|
// "Monday": ["0-6", "20-24"],
|
|
|
|
Json::Value::Members names = scheduleConfiguration.getMemberNames();
|
|
|
|
for (Json::Value::Members::const_iterator it = names.begin();
|
|
it != names.end(); ++it)
|
|
{
|
|
for (Json::Value::ArrayIndex i = 0; i < scheduleConfiguration[*it].size(); i++)
|
|
{
|
|
runningPeriods_.push_back(RunningPeriod(*it, scheduleConfiguration[*it][i].asString()));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isInPeriod()
|
|
{
|
|
if (runningPeriods_.size() == 0)
|
|
{
|
|
return true; // if no config: always run
|
|
}
|
|
|
|
for (std::list<RunningPeriod>::const_iterator it = runningPeriods_.begin();
|
|
it != runningPeriods_.end(); ++it)
|
|
{
|
|
if (it->isInPeriod())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
RunningPeriods runningPeriods_;
|
|
|
|
|
|
struct DbConfiguration
|
|
{
|
|
std::string orthancVersion;
|
|
std::string patientsMainDicomTagsSignature;
|
|
std::string studiesMainDicomTagsSignature;
|
|
std::string seriesMainDicomTagsSignature;
|
|
std::string instancesMainDicomTagsSignature;
|
|
std::string ingestTranscoding;
|
|
std::string dicomWebVersion;
|
|
bool storageCompressionEnabled;
|
|
|
|
DbConfiguration()
|
|
: storageCompressionEnabled(false)
|
|
{
|
|
}
|
|
|
|
bool IsDefined() const
|
|
{
|
|
return !orthancVersion.empty();
|
|
}
|
|
|
|
void Clear()
|
|
{
|
|
orthancVersion.clear();
|
|
patientsMainDicomTagsSignature.clear();
|
|
studiesMainDicomTagsSignature.clear();
|
|
seriesMainDicomTagsSignature.clear();
|
|
instancesMainDicomTagsSignature.clear();
|
|
ingestTranscoding.clear();
|
|
dicomWebVersion.clear();
|
|
}
|
|
|
|
void ToJson(Json::Value& target)
|
|
{
|
|
if (!IsDefined())
|
|
{
|
|
target = Json::nullValue;
|
|
}
|
|
else
|
|
{
|
|
Json::Value signatures;
|
|
|
|
target = Json::objectValue;
|
|
|
|
// default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
|
|
signatures["Patient"] = patientsMainDicomTagsSignature;
|
|
signatures["Study"] = studiesMainDicomTagsSignature;
|
|
signatures["Series"] = seriesMainDicomTagsSignature;
|
|
signatures["Instance"] = instancesMainDicomTagsSignature;
|
|
|
|
target["MainDicomTagsSignature"] = signatures;
|
|
target["OrthancVersion"] = orthancVersion;
|
|
target["StorageCompressionEnabled"] = storageCompressionEnabled;
|
|
target["IngestTranscoding"] = ingestTranscoding;
|
|
target["DicomWebVersion"] = dicomWebVersion;
|
|
}
|
|
}
|
|
|
|
void FromJson(Json::Value& source)
|
|
{
|
|
if (!source.isNull())
|
|
{
|
|
orthancVersion = source["OrthancVersion"].asString();
|
|
if (source.isMember("DicomWebVersion"))
|
|
{
|
|
dicomWebVersion = source["DicomWebVersion"].asString();
|
|
}
|
|
else
|
|
{
|
|
dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
|
|
}
|
|
|
|
const Json::Value& signatures = source["MainDicomTagsSignature"];
|
|
patientsMainDicomTagsSignature = signatures["Patient"].asString();
|
|
studiesMainDicomTagsSignature = signatures["Study"].asString();
|
|
seriesMainDicomTagsSignature = signatures["Series"].asString();
|
|
instancesMainDicomTagsSignature = signatures["Instance"].asString();
|
|
|
|
storageCompressionEnabled = source["StorageCompressionEnabled"].asBool();
|
|
ingestTranscoding = source["IngestTranscoding"].asString();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
struct PluginStatus
|
|
{
|
|
int statusVersion;
|
|
int64_t lastProcessedChange;
|
|
int64_t lastChangeToProcess;
|
|
boost::posix_time::ptime lastTimeStarted;
|
|
|
|
DbConfiguration currentlyProcessingConfiguration; // last configuration being processed (has not reached last change yet)
|
|
DbConfiguration lastProcessedConfiguration; // last configuration that has been fully processed (till last change)
|
|
|
|
PluginStatus()
|
|
: statusVersion(1),
|
|
lastProcessedChange(-1),
|
|
lastChangeToProcess(-1),
|
|
lastTimeStarted(boost::date_time::not_a_date_time)
|
|
{
|
|
}
|
|
|
|
void ToJson(Json::Value& target)
|
|
{
|
|
target = Json::objectValue;
|
|
|
|
target["Version"] = statusVersion;
|
|
target["LastProcessedChange"] = Json::Value::Int64(lastProcessedChange);
|
|
target["LastChangeToProcess"] = Json::Value::Int64(lastChangeToProcess);
|
|
|
|
if (lastTimeStarted == boost::date_time::not_a_date_time)
|
|
{
|
|
target["LastTimeStarted"] = Json::Value::null;
|
|
}
|
|
else
|
|
{
|
|
target["LastTimeStarted"] = boost::posix_time::to_iso_string(lastTimeStarted);
|
|
}
|
|
|
|
currentlyProcessingConfiguration.ToJson(target["CurrentlyProcessingConfiguration"]);
|
|
lastProcessedConfiguration.ToJson(target["LastProcessedConfiguration"]);
|
|
}
|
|
|
|
void FromJson(Json::Value& source)
|
|
{
|
|
statusVersion = source["Version"].asInt();
|
|
lastProcessedChange = source["LastProcessedChange"].asInt64();
|
|
lastChangeToProcess = source["LastChangeToProcess"].asInt64();
|
|
if (source["LastTimeStarted"].isNull())
|
|
{
|
|
lastTimeStarted = boost::date_time::not_a_date_time;
|
|
}
|
|
else
|
|
{
|
|
lastTimeStarted = boost::posix_time::from_iso_string(source["LastTimeStarted"].asString());
|
|
}
|
|
|
|
Json::Value& current = source["CurrentlyProcessingConfiguration"];
|
|
Json::Value& last = source["LastProcessedConfiguration"];
|
|
|
|
currentlyProcessingConfiguration.FromJson(current);
|
|
lastProcessedConfiguration.FromJson(last);
|
|
}
|
|
};
|
|
|
|
static PluginStatus pluginStatus_;
|
|
static boost::recursive_mutex pluginStatusMutex_;
|
|
|
|
static void ReadStatusFromDb()
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
OrthancPlugins::OrthancString globalPropertyContent;
|
|
|
|
globalPropertyContent.Assign(OrthancPluginGetGlobalProperty(OrthancPlugins::GetGlobalContext(),
|
|
globalPropertyId_,
|
|
""));
|
|
|
|
if (!globalPropertyContent.IsNullOrEmpty())
|
|
{
|
|
Json::Value jsonStatus;
|
|
globalPropertyContent.ToJson(jsonStatus);
|
|
pluginStatus_.FromJson(jsonStatus);
|
|
}
|
|
else
|
|
{
|
|
// default config
|
|
pluginStatus_.statusVersion = 1;
|
|
pluginStatus_.lastProcessedChange = -1;
|
|
pluginStatus_.lastChangeToProcess = -1;
|
|
pluginStatus_.lastTimeStarted = boost::date_time::not_a_date_time;
|
|
|
|
pluginStatus_.lastProcessedConfiguration.orthancVersion = "1.9.0"; // when we don't know, we assume some files were stored with Orthanc 1.9.0 (last version saving the dicom-as-json files)
|
|
pluginStatus_.lastProcessedConfiguration.dicomWebVersion = "1.14"; // the first change that requires processing has been introduced between 1.14 & 1.15
|
|
|
|
// default main dicom tags signature are the one from Orthanc 1.4.2 (last time the list was changed):
|
|
pluginStatus_.lastProcessedConfiguration.patientsMainDicomTagsSignature = "0010,0010;0010,0020;0010,0030;0010,0040;0010,1000";
|
|
pluginStatus_.lastProcessedConfiguration.studiesMainDicomTagsSignature = "0008,0020;0008,0030;0008,0050;0008,0080;0008,0090;0008,1030;0020,000d;0020,0010;0032,1032;0032,1060";
|
|
pluginStatus_.lastProcessedConfiguration.seriesMainDicomTagsSignature = "0008,0021;0008,0031;0008,0060;0008,0070;0008,1010;0008,103e;0008,1070;0018,0010;0018,0015;0018,0024;0018,1030;0018,1090;0018,1400;0020,000e;0020,0011;0020,0037;0020,0105;0020,1002;0040,0254;0054,0081;0054,0101;0054,1000";
|
|
pluginStatus_.lastProcessedConfiguration.instancesMainDicomTagsSignature = "0008,0012;0008,0013;0008,0018;0020,0012;0020,0013;0020,0032;0020,0037;0020,0100;0020,4000;0028,0008;0054,1330";
|
|
}
|
|
}
|
|
|
|
static void SaveStatusInDb()
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
Json::Value jsonStatus;
|
|
pluginStatus_.ToJson(jsonStatus);
|
|
|
|
Json::StreamWriterBuilder builder;
|
|
builder.settings_["indentation"] = " ";
|
|
std::string serializedStatus = Json::writeString(builder, jsonStatus);
|
|
|
|
OrthancPluginSetGlobalProperty(OrthancPlugins::GetGlobalContext(),
|
|
globalPropertyId_,
|
|
serializedStatus.c_str());
|
|
}
|
|
|
|
static void GetCurrentDbConfiguration(DbConfiguration& configuration)
|
|
{
|
|
Json::Value signatures;
|
|
Json::Value systemInfo;
|
|
|
|
OrthancPlugins::RestApiGet(systemInfo, "/system", false);
|
|
configuration.patientsMainDicomTagsSignature = systemInfo["MainDicomTags"]["Patient"].asString();
|
|
configuration.studiesMainDicomTagsSignature = systemInfo["MainDicomTags"]["Study"].asString();
|
|
configuration.seriesMainDicomTagsSignature = systemInfo["MainDicomTags"]["Series"].asString();
|
|
configuration.instancesMainDicomTagsSignature = systemInfo["MainDicomTags"]["Instance"].asString();
|
|
configuration.storageCompressionEnabled = systemInfo["StorageCompression"].asBool();
|
|
configuration.ingestTranscoding = systemInfo["IngestTranscoding"].asString();
|
|
|
|
configuration.orthancVersion = OrthancPlugins::GetGlobalContext()->orthancVersion;
|
|
|
|
Json::Value pluginInfo;
|
|
if (OrthancPlugins::RestApiGet(pluginInfo, "/plugins/dicom-web", false))
|
|
{
|
|
configuration.dicomWebVersion = pluginInfo["Version"].asString();
|
|
}
|
|
}
|
|
|
|
static void CheckNeedsProcessing(bool& needsReconstruct, bool& needsReingest, bool& needsDicomWebCaching, const DbConfiguration& current, const DbConfiguration& last)
|
|
{
|
|
needsReconstruct = false;
|
|
needsReingest = false;
|
|
needsDicomWebCaching = false;
|
|
|
|
if (!last.IsDefined())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const char* lastVersion = last.orthancVersion.c_str();
|
|
|
|
if (!OrthancPlugins::CheckMinimalVersion(lastVersion, 1, 9, 1))
|
|
{
|
|
if (triggerOnUnnecessaryDicomAsJsonFiles_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping");
|
|
needsReconstruct = true; // the default reconstruct removes the dicom-as-json
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled");
|
|
}
|
|
}
|
|
|
|
if (last.patientsMainDicomTagsSignature != current.patientsMainDicomTagsSignature)
|
|
{
|
|
if (triggerOnMainDicomTagsChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping");
|
|
needsReconstruct = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Patient main dicom tags have changed but the trigger is disabled");
|
|
}
|
|
}
|
|
|
|
if (last.studiesMainDicomTagsSignature != current.studiesMainDicomTagsSignature)
|
|
{
|
|
if (triggerOnMainDicomTagsChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping");
|
|
needsReconstruct = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Study main dicom tags have changed but the trigger is disabled");
|
|
}
|
|
}
|
|
|
|
if (last.seriesMainDicomTagsSignature != current.seriesMainDicomTagsSignature)
|
|
{
|
|
if (triggerOnMainDicomTagsChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping");
|
|
needsReconstruct = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Series main dicom tags have changed but the trigger is disabled");
|
|
}
|
|
}
|
|
|
|
if (last.instancesMainDicomTagsSignature != current.instancesMainDicomTagsSignature)
|
|
{
|
|
if (triggerOnMainDicomTagsChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping");
|
|
needsReconstruct = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: Instance main dicom tags have changed but the trigger is disabled");
|
|
}
|
|
}
|
|
|
|
if (current.storageCompressionEnabled != last.storageCompressionEnabled)
|
|
{
|
|
if (triggerOnStorageCompressionChange_)
|
|
{
|
|
if (current.storageCompressionEnabled)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression is now enabled -> will perform housekeeping");
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression is now disabled -> will perform housekeeping");
|
|
}
|
|
|
|
needsReingest = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: storage compression has changed but the trigger is disabled");
|
|
}
|
|
}
|
|
|
|
if (current.ingestTranscoding != last.ingestTranscoding)
|
|
{
|
|
if (triggerOnIngestTranscodingChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: ingest transcoding has changed -> will perform housekeeping");
|
|
|
|
needsReingest = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: ingest transcoding has changed but the trigger is disabled");
|
|
}
|
|
}
|
|
|
|
if (!current.dicomWebVersion.empty())
|
|
{
|
|
if (last.dicomWebVersion.empty())
|
|
{
|
|
if (triggerOnDicomWebCacheChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin is enabled and the housekeeper has never run, you might miss series metadata cache -> will perform housekeeping");
|
|
}
|
|
needsDicomWebCaching = triggerOnDicomWebCacheChange_;
|
|
}
|
|
else
|
|
{
|
|
const char* lastDicomWebVersion = last.dicomWebVersion.c_str();
|
|
|
|
if (!OrthancPlugins::CheckMinimalVersion(lastDicomWebVersion, 1, 15, 0))
|
|
{
|
|
if (triggerOnDicomWebCacheChange_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin might miss series metadata cache -> will perform housekeeping");
|
|
needsDicomWebCaching = true;
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: DicomWEB plugin might miss series metadata cache but the trigger has been disabled");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool ProcessChanges(bool needsReconstruct, bool needsReingest, bool needsDicomWebCaching, const DbConfiguration& currentDbConfiguration)
|
|
{
|
|
Json::Value changes;
|
|
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
pluginStatus_.currentlyProcessingConfiguration = currentDbConfiguration;
|
|
|
|
OrthancPlugins::RestApiGet(changes, "/changes?since=" + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) + "&limit=100", false);
|
|
}
|
|
|
|
if (changes["Changes"].size() > 0)
|
|
{
|
|
for (Json::ArrayIndex i = 0; i < changes["Changes"].size(); i++)
|
|
{
|
|
const Json::Value& change = changes["Changes"][i];
|
|
int64_t seq = change["Seq"].asInt64();
|
|
|
|
try
|
|
{
|
|
if (!limitToChange_.empty()) // if updating only maindicomtags for a single level
|
|
{
|
|
if (change["ChangeType"] == limitToChange_)
|
|
{
|
|
Json::Value result;
|
|
Json::Value request;
|
|
request["ReconstructFiles"] = false;
|
|
request["LimitToThisLevelMainDicomTags"] = true;
|
|
OrthancPlugins::RestApiPost(result, "/" + limitToUrl_ + "/" + change["ID"].asString() + "/reconstruct", request, false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
|
|
{
|
|
Json::Value result;
|
|
|
|
if (needsReconstruct || needsReingest ||force_)
|
|
{
|
|
Json::Value request;
|
|
if (needsReingest)
|
|
{
|
|
request["ReconstructFiles"] = true;
|
|
}
|
|
OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
|
|
}
|
|
|
|
if (needsDicomWebCaching)
|
|
{
|
|
Json::Value request;
|
|
OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_ERROR("Housekeeper: unhandled error while processing change " + boost::lexical_cast<std::string>(seq) + ", skipping resource.");
|
|
}
|
|
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
pluginStatus_.lastProcessedChange = seq;
|
|
|
|
if (seq >= pluginStatus_.lastChangeToProcess) // we are done !
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (change["ChangeType"] == "NewStudy")
|
|
{
|
|
boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 1000));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if the change list is empty and Done is true, it means that there is nothing to process anymore
|
|
if (changes["Done"].asBool())
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
pluginStatus_.lastProcessedChange = changes["Last"].asInt64();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static void WorkerThread()
|
|
{
|
|
OrthancPluginSetCurrentThreadName(OrthancPlugins::GetGlobalContext(), "HOUSEKEEPER");
|
|
|
|
DbConfiguration currentDbConfiguration;
|
|
|
|
OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Starting Housekeeper worker thread");
|
|
|
|
ReadStatusFromDb();
|
|
|
|
GetCurrentDbConfiguration(currentDbConfiguration);
|
|
|
|
bool needsReconstruct = false;
|
|
bool needsReingest = false;
|
|
bool needsFullProcessing = false;
|
|
bool needsProcessing = false;
|
|
bool needsDicomWebCaching = false;
|
|
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
// compare with last full processed configuration
|
|
CheckNeedsProcessing(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration, pluginStatus_.lastProcessedConfiguration);
|
|
needsFullProcessing = needsReconstruct || needsReingest || needsDicomWebCaching;
|
|
needsProcessing = needsFullProcessing;
|
|
|
|
if (needsFullProcessing && !limitToChange_.empty())
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: full processing needed -> ignoring the \"LimitMainDicomTagsReconstructLevel\" configuration");
|
|
limitToChange_.clear();
|
|
limitToUrl_.clear();
|
|
}
|
|
|
|
// if a processing was in progress, check if the config has changed since
|
|
if (pluginStatus_.currentlyProcessingConfiguration.IsDefined())
|
|
{
|
|
needsProcessing = true; // since a processing was in progress, we need at least a partial processing
|
|
|
|
bool needsReconstruct2 = false;
|
|
bool needsReingest2 = false;
|
|
bool needsDicomWebCaching2 = false;
|
|
|
|
CheckNeedsProcessing(needsReconstruct2, needsReingest2, needsDicomWebCaching2, currentDbConfiguration, pluginStatus_.currentlyProcessingConfiguration);
|
|
needsFullProcessing = needsReconstruct2 || needsReingest2 || needsDicomWebCaching2; // if the configuration has changed compared to the config being processed, we need a full processing again
|
|
}
|
|
}
|
|
|
|
if (!needsProcessing && !force_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: everything has been processed already !");
|
|
return;
|
|
}
|
|
|
|
if (force_ || needsFullProcessing)
|
|
{
|
|
if (force_)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: forcing execution -> will perform housekeeping");
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the Orthanc configuration has changed since last run, will reprocess the whole DB !");
|
|
}
|
|
|
|
Json::Value changes;
|
|
OrthancPlugins::RestApiGet(changes, "/changes?last", false);
|
|
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
pluginStatus_.lastProcessedChange = 0;
|
|
pluginStatus_.lastChangeToProcess = changes["Last"].asInt64(); // the last change is the last change at the time we start. We assume that every new ingested file will be constructed correctly
|
|
pluginStatus_.lastTimeStarted = boost::posix_time::microsec_clock::universal_time();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper: the Orthanc configuration has not changed since last run, will continue processing changes");
|
|
}
|
|
|
|
bool completed = false;
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
completed = pluginStatus_.lastChangeToProcess == 0; // if the DB is empty at start, no need to process anyting
|
|
}
|
|
|
|
bool loggedNotRightPeriodChangeMessage = false;
|
|
|
|
while (!workerThreadShouldStop_ && !completed)
|
|
{
|
|
if (runningPeriods_.isInPeriod())
|
|
{
|
|
try
|
|
{
|
|
completed = ProcessChanges(needsReconstruct, needsReingest, needsDicomWebCaching, currentDbConfiguration);
|
|
SaveStatusInDb();
|
|
}
|
|
catch (...)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_ERROR("Housekeeper: unhandled error while processing change " + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) +
|
|
" / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
|
|
}
|
|
|
|
if (!completed)
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
ORTHANC_PLUGINS_LOG_INFO("Housekeeper: processed changes " + boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) +
|
|
" / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
|
|
|
|
boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 100)); // wait 1/10 of the delay between changes
|
|
}
|
|
|
|
loggedNotRightPeriodChangeMessage = false;
|
|
}
|
|
else
|
|
{
|
|
if (!loggedNotRightPeriodChangeMessage)
|
|
{
|
|
ORTHANC_PLUGINS_LOG_INFO("Housekeeper: entering quiet period");
|
|
loggedNotRightPeriodChangeMessage = true;
|
|
}
|
|
|
|
boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
|
|
}
|
|
}
|
|
|
|
if (completed)
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
pluginStatus_.lastProcessedConfiguration = currentDbConfiguration;
|
|
pluginStatus_.currentlyProcessingConfiguration.Clear();
|
|
|
|
pluginStatus_.lastProcessedChange = -1;
|
|
pluginStatus_.lastChangeToProcess = -1;
|
|
|
|
SaveStatusInDb();
|
|
|
|
OrthancPluginLogWarning(OrthancPlugins::GetGlobalContext(), "Housekeeper: finished processing all changes");
|
|
}
|
|
}
|
|
|
|
extern "C"
|
|
{
|
|
OrthancPluginErrorCode GetPluginStatus(OrthancPluginRestOutput* output,
|
|
const char* url,
|
|
const OrthancPluginHttpRequest* request)
|
|
{
|
|
if (request->method != OrthancPluginHttpMethod_Get)
|
|
{
|
|
OrthancPlugins::AnswerMethodNotAllowed(output, "GET");
|
|
}
|
|
else
|
|
{
|
|
boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
|
|
|
|
Json::Value status;
|
|
pluginStatus_.ToJson(status);
|
|
|
|
OrthancPlugins::AnswerJson(status, output);
|
|
}
|
|
|
|
return OrthancPluginErrorCode_Success;
|
|
}
|
|
|
|
|
|
OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
|
|
OrthancPluginResourceType resourceType,
|
|
const char* resourceId)
|
|
{
|
|
switch (changeType)
|
|
{
|
|
case OrthancPluginChangeType_OrthancStarted:
|
|
{
|
|
workerThread_.reset(new boost::thread(WorkerThread));
|
|
return OrthancPluginErrorCode_Success;
|
|
}
|
|
case OrthancPluginChangeType_OrthancStopped:
|
|
{
|
|
if (workerThread_ && workerThread_->joinable())
|
|
{
|
|
workerThreadShouldStop_ = true;
|
|
workerThread_->join();
|
|
}
|
|
}
|
|
default:
|
|
return OrthancPluginErrorCode_Success;
|
|
}
|
|
}
|
|
|
|
ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
|
|
{
|
|
OrthancPlugins::SetGlobalContext(c, HOUSEKEEPER_NAME);
|
|
|
|
/* Check the version of the Orthanc core */
|
|
if (OrthancPluginCheckVersion(c) == 0)
|
|
{
|
|
OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
|
|
ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
|
|
ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
|
|
return -1;
|
|
}
|
|
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is initializing");
|
|
OrthancPluginSetDescription2(c, HOUSEKEEPER_NAME, "Optimizes your DB and storage.");
|
|
|
|
OrthancPlugins::OrthancConfiguration orthancConfiguration;
|
|
|
|
OrthancPlugins::OrthancConfiguration housekeeper;
|
|
orthancConfiguration.GetSection(housekeeper, "Housekeeper");
|
|
|
|
bool enabled = housekeeper.GetBooleanValue("Enable", false);
|
|
if (enabled)
|
|
{
|
|
/*
|
|
{
|
|
"Housekeeper": {
|
|
|
|
// Enables/disables the plugin
|
|
"Enable": false,
|
|
|
|
// the Global Prooperty ID in which the plugin progress
|
|
// is stored. Must be > 1024 and must not be used by
|
|
// another plugin
|
|
"GlobalPropertyId": 1025,
|
|
|
|
// Forces execution even if the plugin did not detect
|
|
// any changes in configuration
|
|
"Force": false,
|
|
|
|
// Delay (in seconds) between reconstruction of 2 studies
|
|
// This avoids overloading Orthanc with the housekeeping
|
|
// process and leaves room for other operations.
|
|
"ThrottleDelay": 5,
|
|
|
|
// Runs the plugin only at certain period of time.
|
|
// If not specified, the plugin runs all the time
|
|
// Examples:
|
|
// to run between 0AM and 6AM everyday + every night
|
|
// from 8PM to 12PM and 24h a day on the weekend:
|
|
// "Schedule": {
|
|
// "Monday": ["0-6", "20-24"],
|
|
// "Tuesday": ["0-6", "20-24"],
|
|
// "Wednesday": ["0-6", "20-24"],
|
|
// "Thursday": ["0-6", "20-24"],
|
|
// "Friday": ["0-6", "20-24"],
|
|
// "Saturday": ["0-24"],
|
|
// "Sunday": ["0-24"]
|
|
// },
|
|
|
|
// configure events that can trigger a housekeeping processing
|
|
"Triggers" : {
|
|
"StorageCompressionChange": true,
|
|
"MainDicomTagsChange": true,
|
|
"UnnecessaryDicomAsJsonFiles": true,
|
|
"IngestTranscodingChange": true,
|
|
"DicomWebCacheChange": true // new in 1.12.2
|
|
},
|
|
|
|
// When rebuilding MainDicomTags, limit to a single level of resource
|
|
// which can greatly improve performances e.g. if you have only updated
|
|
// the Study level ExtraMainDicomTags.
|
|
// Allowed values: "Patient", "Study", "Series", "Instance", "All"
|
|
"LimitMainDicomTagsReconstructLevel": "All"
|
|
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
globalPropertyId_ = housekeeper.GetIntegerValue("GlobalPropertyId", 1025);
|
|
force_ = housekeeper.GetBooleanValue("Force", false);
|
|
throttleDelay_ = housekeeper.GetUnsignedIntegerValue("ThrottleDelay", 5);
|
|
|
|
if (housekeeper.GetJson().isMember("Triggers"))
|
|
{
|
|
OrthancPlugins::OrthancConfiguration triggers;
|
|
housekeeper.GetSection(triggers, "Triggers");
|
|
triggerOnStorageCompressionChange_ = triggers.GetBooleanValue("StorageCompressionChange", true);
|
|
|
|
triggerOnMainDicomTagsChange_ = triggers.GetBooleanValue("MainDicomTagsChange", true);
|
|
triggerOnUnnecessaryDicomAsJsonFiles_ = triggers.GetBooleanValue("UnnecessaryDicomAsJsonFiles", true);
|
|
triggerOnIngestTranscodingChange_ = triggers.GetBooleanValue("IngestTranscodingChange", true);
|
|
triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
|
|
}
|
|
|
|
limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "All");
|
|
if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
|
|
&& limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance" && limitMainDicomTagsReconstructLevel_ != "All")
|
|
{
|
|
ORTHANC_PLUGINS_LOG_ERROR("Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" + limitMainDicomTagsReconstructLevel_ + "'");
|
|
return -1;
|
|
}
|
|
else if (limitMainDicomTagsReconstructLevel_ == "Patient")
|
|
{
|
|
limitToChange_ = "NewPatient";
|
|
limitToUrl_ = "patients";
|
|
}
|
|
else if (limitMainDicomTagsReconstructLevel_ == "Study")
|
|
{
|
|
limitToChange_ = "NewStudy";
|
|
limitToUrl_ = "studies";
|
|
}
|
|
else if (limitMainDicomTagsReconstructLevel_ == "Series")
|
|
{
|
|
limitToChange_ = "NewSeries";
|
|
limitToUrl_ = "series";
|
|
}
|
|
else if (limitMainDicomTagsReconstructLevel_ == "Instance")
|
|
{
|
|
limitToChange_ = "NewInstance";
|
|
limitToUrl_ = "instances";
|
|
}
|
|
|
|
if (housekeeper.GetJson().isMember("Schedule"))
|
|
{
|
|
runningPeriods_.load(housekeeper.GetJson()["Schedule"]);
|
|
}
|
|
|
|
OrthancPluginRegisterOnChangeCallback(c, OnChangeCallback);
|
|
OrthancPluginRegisterRestCallback(c, "/housekeeper/status", GetPluginStatus); // for backward compatiblity with version 1.11.0
|
|
OrthancPluginRegisterRestCallback(c, "/plugins/housekeeper/status", GetPluginStatus);
|
|
}
|
|
else
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is disabled by the configuration file");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
ORTHANC_PLUGINS_API void OrthancPluginFinalize()
|
|
{
|
|
ORTHANC_PLUGINS_LOG_WARNING("Housekeeper plugin is finalizing");
|
|
}
|
|
|
|
|
|
ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
|
|
{
|
|
return HOUSEKEEPER_NAME;
|
|
}
|
|
|
|
|
|
ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
|
|
{
|
|
return HOUSEKEEPER_VERSION;
|
|
}
|
|
}
|